Bladeren bron

adding unit test

namnguyen 11 maanden geleden
bovenliggende
commit
2a7e04bc09

+ 6 - 5
frontend/taipy-gui/src/components/Taipy/Metric.tsx

@@ -40,7 +40,7 @@ interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
     height?: string | number;
     height?: string | number;
     showValue?: boolean;
     showValue?: boolean;
     format?: string;
     format?: string;
-    formatDelta?: string;
+    deltaFormat?: string;
 }
 }
 
 
 const emptyLayout = {} as Record<string, Record<string, unknown>>;
 const emptyLayout = {} as Record<string, Record<string, unknown>>;
@@ -73,9 +73,9 @@ const Metric = (props: MetricProps) => {
                 },
                 },
                 delta: {
                 delta: {
                     reference: typeof value === 'number' && typeof delta === 'number' ? value - delta : undefined,
                     reference: typeof value === 'number' && typeof delta === 'number' ? value - delta : undefined,
-                    prefix: extractPrefix(props.formatDelta),
-                    suffix: extractSuffix(props.formatDelta),
-                    valueformat: extractFormatSpecifier(props.formatDelta)
+                    prefix: extractPrefix(props.deltaFormat),
+                    suffix: extractSuffix(props.deltaFormat),
+                    valueformat: extractFormatSpecifier(props.deltaFormat)
                 },
                 },
                 gauge: {
                 gauge: {
                     axis: {
                     axis: {
@@ -95,7 +95,7 @@ const Metric = (props: MetricProps) => {
         ];
         ];
     }, [
     }, [
         props.format,
         props.format,
-        props.formatDelta,
+        props.deltaFormat,
         props.min,
         props.min,
         props.max,
         props.max,
         props.type,
         props.type,
@@ -122,6 +122,7 @@ const Metric = (props: MetricProps) => {
                     data={data as Data[]}
                     data={data as Data[]}
                     layout={baseLayout}
                     layout={baseLayout}
                     style={style}
                     style={style}
+                    useResizeHandler
                 />
                 />
             </Suspense>
             </Suspense>
         </Box>
         </Box>

+ 30 - 52
frontend/taipy-gui/src/utils/formatConversion.ts

@@ -11,51 +11,46 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-const FORMAT_REPLACE_REGEX = /%([0-9]*)([.][0-9]+)?([bdieufgosxX])/g;
-
 /**
 /**
- * Converts a sprintf format string to a D3 format string.
- *
- * This function takes a sprintf format string as input and returns a D3 format string.
- * The conversion is done by replacing each sprintf format specifier with the corresponding D3 format specifier.
- * If the input format string is undefined, the function returns undefined.
- * If an error occurs during the conversion, the function logs the error to the console and returns undefined.
+ * Convert sprintf-like format string to D3 format string.
  *
  *
- * @param format - The sprintf format string to convert.
- * @returns The converted D3 format string, or undefined if the input is undefined or if an error occurs.
+ * @param format - The sprintf-like format string.
+ * @returns The converted D3 format string or undefined if an error occurs.
  */
  */
 const sprintfToD3Converter = (format: string) => {
 const sprintfToD3Converter = (format: string) => {
-    try {
-        return format?.replace(FORMAT_REPLACE_REGEX, (match, width, precision, type) => {
+    /**
+     * Helper function to handle precision formatting.
+     *
+     * @param precision - The precision part of the format string.
+     * @param specifier - The type of formatting.
+     * @returns The D3 precision format string.
+     */
+    const precisionFormat = (precision: string | undefined, specifier: string) => {
+        // Default to precision of 2 if not specified
+        return "." + (precision?.slice(1) ?? "2") + specifier;
+    }
+
+        return format?.replace(/%([0-9]*)([.][0-9]+)?([bdieufgoxX])/g, (match, width, precision, type) => {
+            console.log(`Match: ${match}, Width: ${width}, Precision: ${precision}, Type: ${type}`)
             switch (type) {
             switch (type) {
                 case "b":
                 case "b":
-                    return "b";
                 case "d":
                 case "d":
-                    return "d";
                 case "i":
                 case "i":
-                    return "d";
                 case "e":
                 case "e":
-                    return "e";
-                case "f":
-                    return "." + (precision?.slice(1) ?? "2") + "f";
-                case "g":
-                    return "." + (precision?.slice(1) ?? "2") + "g";
                 case "o":
                 case "o":
-                    return "o";
-                case "s":
-                    return "";
                 case "x":
                 case "x":
-                    return "x";
                 case "X":
                 case "X":
-                    return "X";
+                    return type;
+                case "f":
+                    return precisionFormat(precision, "f");
+                case "g":
+                    return precisionFormat(precision, "g");
+                case "u":
+                    return "("
                 default:
                 default:
                     return "";
                     return "";
             }
             }
         });
         });
-    } catch (error) {
-        console.error(`Failed to convert format "${format}" to D3 format: ${error}`);
-        return undefined;
-    }
 }
 }
 
 
 /**
 /**
@@ -67,14 +62,8 @@ export const extractPrefix = (format: string | undefined): string | undefined =>
     if (format === undefined) {
     if (format === undefined) {
         return undefined;
         return undefined;
     }
     }
-
-    try {
         const PREFIX_MATCH_REGEX: RegExp = /.*?(?=%)/;
         const PREFIX_MATCH_REGEX: RegExp = /.*?(?=%)/;
         return format.match(PREFIX_MATCH_REGEX)?.[0] ?? "";
         return format.match(PREFIX_MATCH_REGEX)?.[0] ?? "";
-    } catch (error) {
-        console.error(`Failed to extract prefix from format "${format}": ${error}`);
-        return undefined;
-    }
 }
 }
 
 
 /**
 /**
@@ -86,20 +75,14 @@ export const extractSuffix = (format: string | undefined): string | undefined =>
     if (format === undefined) {
     if (format === undefined) {
         return undefined;
         return undefined;
     }
     }
-
-    try {
-        const SURFIX_MATCH_REGEX: RegExp = /(?<=[bdieufgosxX])./;
+        const SURFIX_MATCH_REGEX: RegExp = /(?<=[bdieufgsxX])./;
         return format.match(SURFIX_MATCH_REGEX)?.[0] ?? "";
         return format.match(SURFIX_MATCH_REGEX)?.[0] ?? "";
-    } catch (error) {
-        console.error(`Failed to extract suffix from format "${format}": ${error}`);
-        return undefined;
-    }
 }
 }
 
 
 /**
 /**
  * Extracts the format specifier from the input string.
  * Extracts the format specifier from the input string.
  * The input string is expected to be in sprintf format.
  * The input string is expected to be in sprintf format.
- * The function returns the format specifier (one of 'b', 'd', 'i', 'e', 'f', 'g', 'o', 's', 'x', 'X') if it exists, or undefined otherwise.
+ * The function returns the format specifier (one of 'b', 'd', 'i', 'e', 'u', 'f', 'g', 'o', 'x', 'X') if it exists, or undefined otherwise.
  * @param input - The input string in sprintf format.
  * @param input - The input string in sprintf format.
  * @returns The extracted format specifier, or undefined if no specifier is found or if the input is undefined.
  * @returns The extracted format specifier, or undefined if no specifier is found or if the input is undefined.
  */
  */
@@ -107,13 +90,8 @@ export const extractFormatSpecifier = (input: string | undefined): string | unde
     if (input === undefined) {
     if (input === undefined) {
         return undefined;
         return undefined;
     }
     }
-
-    try {
-        const regex: RegExp = /.*%?([bdieufgosxX]).*/g;
-        const convertedInput = sprintfToD3Converter(input);
-        return convertedInput ? convertedInput.replace(regex, '$1') : undefined;
-    } catch (error) {
-        console.error(`Failed to extract format specifier from input "${input}": ${error}`);
-        return undefined;
-    }
+        const regex: RegExp = /%.*\.?[bdieufgoxX]/;
+        const match = input.match(regex);
+        const format = match ? match[0] : undefined;
+        return format ? sprintfToD3Converter(format) : undefined;
 }
 }

+ 1 - 0
pytest.ini

@@ -9,5 +9,6 @@ filterwarnings =
     ignore::FutureWarning:pyarrow
     ignore::FutureWarning:pyarrow
 markers =
 markers =
     teste2e:End-to-end tests
     teste2e:End-to-end tests
+    extension:Taipy GUI Extension tests
     orchestrator_dispatcher:Orchestrator dispatcher tests
     orchestrator_dispatcher:Orchestrator dispatcher tests
     standalone:Tests starting a standalone dispatcher thread
     standalone:Tests starting a standalone dispatcher thread

+ 1 - 1
taipy/gui/_renderers/factory.py

@@ -372,7 +372,7 @@ class _Factory:
                 ("height", PropertyType.string_or_number),
                 ("height", PropertyType.string_or_number),
                 ("show_value", PropertyType.boolean, True),
                 ("show_value", PropertyType.boolean, True),
                 ("format", PropertyType.string),
                 ("format", PropertyType.string),
-                ("format_delta", PropertyType.string),
+                ("delta_format", PropertyType.string),
             ]
             ]
         ),
         ),
         "navbar": lambda gui, control_type, attrs: _Builder(
         "navbar": lambda gui, control_type, attrs: _Builder(

+ 12 - 0
taipy/gui/viselements.json

@@ -895,6 +895,18 @@
                         "type": "str|number",
                         "type": "str|number",
                         "default_value": "None",
                         "default_value": "None",
                         "doc": "The height, in CSS units, of the metric."
                         "doc": "The height, in CSS units, of the metric."
+                    },
+                    {
+                        "name": "format",
+                        "type": "str",
+                        "default_value": "None",
+                        "doc": "The format to use when displaying the value.<br/>This uses the <code>printf</code> syntax."
+                    },
+                    {
+                        "name": "delta_format",
+                        "type": "str",
+                        "default_value": "None",
+                        "doc": "The format to use when displaying the delta value.<br/>This uses the <code>printf</code> syntax."
                     }
                     }
                 ]
                 ]
             }
             }

+ 192 - 44
tests/gui/e2e/test_metric_indicator.py

@@ -14,81 +14,229 @@ from importlib import util
 import pytest
 import pytest
 
 
 if util.find_spec("playwright"):
 if util.find_spec("playwright"):
-  from playwright._impl._page import Page
+    from playwright._impl._page import Page
 
 
 from taipy.gui import Gui
 from taipy.gui import Gui
 
 
 
 
 @pytest.mark.extension
 @pytest.mark.extension
 def test_has_default_value(page: Page, gui: Gui, helpers):
 def test_has_default_value(page: Page, gui: Gui, helpers):
-  page_md = """
+    page_md = """
 <|100|metric|>
 <|100|metric|>
 """
 """
-  gui._set_frame(inspect.currentframe())
-  gui.add_page(name="test", page=page_md)
-  helpers.run_e2e(gui)
-  page.goto("./test")
-  page.wait_for_selector(".plot-container")
-  events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
-  gauge_value = events_list.nth(0).text_content()
-  assert gauge_value == "100"
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
+    gauge_value = events_list.nth(0).text_content()
+    assert gauge_value == "100"
 
 
 
 
 @pytest.mark.extension
 @pytest.mark.extension
 def test_show_increase_delta_value(page: Page, gui: Gui, helpers):
 def test_show_increase_delta_value(page: Page, gui: Gui, helpers):
-  page_md = """
+    page_md = """
 <|100|metric|delta=20|type=linear|>
 <|100|metric|delta=20|type=linear|>
 """
 """
-  gui._set_frame(inspect.currentframe())
-  gui.add_page(name="test", page=page_md)
-  helpers.run_e2e(gui)
-  page.goto("./test")
-  page.wait_for_selector(".plot-container")
-  events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
-  delta_value = events_list.nth(1).text_content()
-  assert delta_value == "▲20"
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
+    delta_value = events_list.nth(1).text_content()
+    assert delta_value == "▲20"
 
 
 
 
 @pytest.mark.extension
 @pytest.mark.extension
 def test_show_decrease_delta_value(page: Page, gui: Gui, helpers):
 def test_show_decrease_delta_value(page: Page, gui: Gui, helpers):
-  page_md = """
+    page_md = """
 <|100|metric|delta=-20|type=linear|>
 <|100|metric|delta=-20|type=linear|>
 """
 """
-  gui._set_frame(inspect.currentframe())
-  gui.add_page(name="test", page=page_md)
-  helpers.run_e2e(gui)
-  page.goto("./test")
-  page.wait_for_selector(".plot-container")
-  events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
-  delta_value = events_list.nth(1).text_content()
-  assert delta_value == "▼−20"
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    events_list = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[local-name()='text']")
+    delta_value = events_list.nth(1).text_content()
+    assert delta_value == "▼−20"
 
 
 
 
 @pytest.mark.extension
 @pytest.mark.extension
 def test_show_linear_chart(page: Page, gui: Gui, helpers):
 def test_show_linear_chart(page: Page, gui: Gui, helpers):
-  page_md = """
+    page_md = """
 <|100|metric|delta=-20|type=linear|>
 <|100|metric|delta=-20|type=linear|>
 """
 """
-  gui._set_frame(inspect.currentframe())
-  gui.add_page(name="test", page=page_md)
-  helpers.run_e2e(gui)
-  page.goto("./test")
-  page.wait_for_selector(".plot-container")
-  chart = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='bullet']")
-  assert chart.is_visible()
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    chart = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='bullet']")
+    assert chart.is_visible()
 
 
 
 
 @pytest.mark.extension
 @pytest.mark.extension
 def test_show_circular_chart_as_default_type(page: Page, gui: Gui, helpers):
 def test_show_circular_chart_as_default_type(page: Page, gui: Gui, helpers):
-  page_md = """
+    page_md = """
 <|100|metric|>
 <|100|metric|>
 """
 """
-  gui._set_frame(inspect.currentframe())
-  gui.add_page(name="test", page=page_md)
-  helpers.run_e2e(gui)
-  page.goto("./test")
-  page.wait_for_selector(".plot-container")
-  chart = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='angular']")
-  assert chart.is_visible()
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    chart = page.locator("//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='angular']")
+    assert chart.is_visible()
 
 
 
 
+@pytest.mark.extension
+def test_format_converter_integer_to_binary(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%b|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "110010"
+
+
+@pytest.mark.extension
+def test_format_converter_integer_to_signed_decimal(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%d|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "50"
+
+
+@pytest.mark.extension
+def test_format_converter_integer_to_signed_decimal(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%i|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "50"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_float_using_science_notation(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%e|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "5.000000e+1"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_float_using_fixed_point_notation(page: Page, gui: Gui, helpers):
+    page_md = """
+<|99.99|metric|show_value=True|delta=20|format=%.2f|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "99.99"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_float_using_fixed_point_notation(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50.555|metric|show_value=True|delta=20|format=%.2g|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "51"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_integer_as_octal(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%o|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "62"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_integer_as_hexadecimal(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%x|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "32"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_integer_as_uppercase_hexadecimal(page: Page, gui: Gui, helpers):
+    page_md = """
+<|50|metric|show_value=True|delta=20|format=%X|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "32"
+
+
+@pytest.mark.extension
+def test_format_converter_yields_integer_as_unsigned_decimal(page: Page, gui: Gui, helpers):
+    page_md = """
+<|-50|metric|show_value=True|delta=20|format=%u|>
+"""
+    gui._set_frame(inspect.currentframe())
+    gui.add_page(name="test", page=page_md)
+    helpers.run_e2e(gui)
+    page.goto("./test")
+    page.wait_for_selector(".plot-container")
+    number = page.locator(
+        "//*[@class='js-plotly-plot']//*[name()='svg'][2]//*[@class='number']")
+    assert number.text_content() == "(50)"