Преглед на файлове

Merge branch 'develop' into bug/#1911-current-page-menu

Nam Nguyen преди 6 месеца
родител
ревизия
c2b6175490

+ 12 - 2
frontend/taipy-gui/src/components/Taipy/Metric.spec.tsx

@@ -286,8 +286,18 @@ describe("Metric Component", () => {
         });
     });
 
-    it("processes type prop correctly when type is none", async () => {
-        const { container } = render(<Metric type="none"  />);
+    it("processes type prop correctly when type is none (string)", async () => {
+        const { container } = render(<Metric type="none" />);
+        await waitFor(() => {
+            const angularElm = container.querySelector(".angular");
+            const angularAxis = container.querySelector(".angularaxis");
+            expect(angularElm).not.toBeInTheDocument();
+            expect(angularAxis).not.toBeInTheDocument();
+        });
+    });
+    
+    it("processes type prop correctly when type is None", async () => {
+        const { container } = render(<Metric type="None" />);
         await waitFor(() => {
             const angularElm = container.querySelector(".angular");
             const angularAxis = container.querySelector(".angularaxis");

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

@@ -87,7 +87,7 @@ const Metric = (props: MetricProps) => {
     }, [props.colorMap, props.max]);
 
     const data = useMemo(() => {
-        const mode = props.type === "none" ? [] : ["gauge"];
+        const mode = typeof props.type === "string" && props.type.toLowerCase() === "none" ? [] : ["gauge"];
         showValue && mode.push("number");
         delta !== undefined && mode.push("delta");
         const deltaIncreasing = props.deltaColor

+ 5 - 2
taipy/gui/_renderers/builder.py

@@ -61,7 +61,7 @@ class _Builder:
 
     __BLOCK_CONTROLS = ["dialog", "expandable", "pane", "part"]
 
-    __TABLE_COLUMNS_DEPS = [
+    __TABLE_COLUMNS_DEPS = {
         "data",
         "columns",
         "date_format",
@@ -75,7 +75,10 @@ class _Builder:
         "style",
         "tooltip",
         "lov",
-    ]
+        "row_class_name",
+        "cell_class_name",
+        "format_fn"
+    }
 
     def __init__(
         self,

+ 7 - 3
taipy/gui/gui.py

@@ -73,7 +73,7 @@ from .extension.library import Element, ElementLibrary
 from .page import Page
 from .partial import Partial
 from .server import _Server
-from .state import State
+from .state import State, _GuiState
 from .types import _WsType
 from .utils import (
     _delscopeattr,
@@ -2292,7 +2292,9 @@ class Gui:
             if isinstance(callback, str)
             else _get_lambda_id(t.cast(LambdaType, callback))
             if _is_unnamed_function(callback)
-            else callback.__name__ if callback is not None else None
+            else callback.__name__
+            if callback is not None
+            else None
         )
         func = self.__get_on_cancel_block_ui(action_name)
         def_action_name = func.__name__
@@ -2809,7 +2811,9 @@ class Gui:
         self.__var_dir.set_default(self.__frame)
 
         if self.__state is None or is_reloading:
-            self.__state = State(self, self.__locals_context.get_all_keys(), self.__locals_context.get_all_context())
+            self.__state = _GuiState(
+                self, self.__locals_context.get_all_keys(), self.__locals_context.get_all_context()
+            )
 
         if _is_in_notebook():
             # Allow gui.state.x in notebook mode

+ 10 - 0
taipy/gui/mock/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# 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.

+ 62 - 0
taipy/gui/mock/mock_state.py

@@ -0,0 +1,62 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# 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 typing as t
+
+from .. import Gui, State
+from ..utils import _MapDict
+
+
+class MockState(State):
+    """A Mock implementation for `State`.
+    TODO
+    example of use:
+    ```py
+    def test_callback():
+        ms = MockState(Gui(""), a = 1)
+        on_action(ms) # function to test
+        assert ms.a == 2
+    ```
+    """
+
+    __VARS = "vars"
+
+    def __init__(self, gui: Gui, **kwargs) -> None:
+        super().__setattr__(MockState.__VARS, {k: _MapDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()})
+        self._gui = gui
+        super().__init__()
+
+    def get_gui(self) -> Gui:
+        return self._gui
+
+    def __getattribute__(self, name: str) -> t.Any:
+        if (attr := t.cast(dict, super().__getattribute__(MockState.__VARS)).get(name, None)) is not None:
+            return attr
+        try:
+            return super().__getattribute__(name)
+        except Exception:
+            return None
+
+    def __setattr__(self, name: str, value: t.Any) -> None:
+        t.cast(dict, super().__getattribute__(MockState.__VARS))[name] = (
+            _MapDict(value) if isinstance(value, dict) else value
+        )
+
+    def __getitem__(self, key: str):
+        return self
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        return True
+
+    def broadcast(self, name: str, value: t.Any):
+        pass

+ 109 - 94
taipy/gui/state.py

@@ -11,6 +11,7 @@
 
 import inspect
 import typing as t
+from abc import ABC, abstractmethod
 from contextlib import nullcontext
 from operator import attrgetter
 from pathlib import Path
@@ -25,7 +26,7 @@ if t.TYPE_CHECKING:
     from .gui import Gui
 
 
-class State:
+class State(ABC):
     """Accessor to the bound variables from callbacks.
 
     `State` is used when you need to access the value of variables
@@ -73,6 +74,87 @@ class State:
     ```
     """
 
+    def __init__(self) -> None:
+        self._gui: "Gui"
+
+    @abstractmethod
+    def get_gui(self) -> "Gui":
+        """Return the Gui instance for this state object.
+
+        Returns:
+            Gui: The Gui instance for this state object.
+        """
+        raise NotImplementedError
+
+    def assign(self, name: str, value: t.Any) -> t.Any:
+        """Assign a value to a state variable.
+
+        This should be used only from within a lambda function used
+        as a callback in a visual element.
+
+        Arguments:
+            name (str): The variable name to assign to.
+            value (Any): The new variable value.
+
+        Returns:
+            Any: The previous value of the variable.
+        """
+        val = attrgetter(name)(self)
+        _attrsetter(self, name, value)
+        return val
+
+    def refresh(self, name: str):
+        """Refresh a state variable.
+
+        This allows to re-sync the user interface with a variable value.
+
+        Arguments:
+            name (str): The variable name to refresh.
+        """
+        val = attrgetter(name)(self)
+        _attrsetter(self, name, val)
+
+    def _set_context(self, gui: "Gui") -> t.ContextManager[None]:
+        return nullcontext()
+
+    def broadcast(self, name: str, value: t.Any):
+        """Update a variable on all clients.
+
+        All connected clients will receive an update of the variable called *name* with the
+        provided value, even if it is not shared.
+
+        Arguments:
+            name (str): The variable name to update.
+            value (Any): The new variable value.
+        """
+        with self._set_context(self._gui):
+            encoded_name = self._gui._bind_var(name)
+            self._gui._broadcast_all_clients(encoded_name, value)
+
+    def __enter__(self):
+        self._gui.__enter__()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        return self._gui.__exit__(exc_type, exc_value, traceback)
+
+    def set_favicon(self, favicon_path: t.Union[str, Path]):
+        """Change the favicon for the client of this state.
+
+        This function dynamically changes the favicon (the icon associated with the application's
+        pages) of Taipy GUI pages for the specific client of this state.
+
+        Note that the *favicon* parameter to `(Gui.)run()^` can also be used to change
+        the favicon when the application starts.
+
+        Arguments:
+            favicon_path: The path to the image file to use.<br/>
+                This can be expressed as a path name or a URL (relative or not).
+        """
+        self._gui.set_favicon(favicon_path, self)
+
+
+class _GuiState(State):
     __gui_attr = "_gui"
     __attrs = (
         __gui_attr,
@@ -100,68 +182,66 @@ class State:
     __excluded_attrs = __attrs + __methods + __placeholder_attrs
 
     def __init__(self, gui: "Gui", var_list: t.Iterable[str], context_list: t.Iterable[str]) -> None:
-        super().__setattr__(State.__attrs[1], list(State.__filter_var_list(var_list, State.__excluded_attrs)))
-        super().__setattr__(State.__attrs[2], list(context_list))
-        super().__setattr__(State.__attrs[0], gui)
-
-    def get_gui(self) -> "Gui":
-        """Return the Gui instance for this state object.
-
-        Returns:
-            Gui: The Gui instance for this state object.
-        """
-        return super().__getattribute__(State.__gui_attr)
+        super().__setattr__(
+            _GuiState.__attrs[1], list(_GuiState.__filter_var_list(var_list, _GuiState.__excluded_attrs))
+        )
+        super().__setattr__(_GuiState.__attrs[2], list(context_list))
+        super().__setattr__(_GuiState.__attrs[0], gui)
+        super().__init__()
 
     @staticmethod
     def __filter_var_list(var_list: t.Iterable[str], excluded_attrs: t.Iterable[str]) -> t.Iterable[str]:
         return filter(lambda n: n not in excluded_attrs, var_list)
 
+    def get_gui(self) -> "Gui":
+        return super().__getattribute__(_GuiState.__gui_attr)
+
     def __getattribute__(self, name: str) -> t.Any:
         if name == "__class__":
-            return State
-        if name in State.__methods:
+            return _GuiState
+        if name in _GuiState.__methods:
             return super().__getattribute__(name)
         gui: "Gui" = self.get_gui()
-        if name == State.__gui_attr:
+        if name == _GuiState.__gui_attr:
             return gui
-        if name in State.__excluded_attrs:
+        if name in _GuiState.__excluded_attrs:
             raise AttributeError(f"Variable '{name}' is protected and is not accessible.")
         if gui._is_in_brdcst_callback() and (
             name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
         ):
             raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
-        if not name.startswith("__") and name not in super().__getattribute__(State.__attrs[1]):
+        if not name.startswith("__") and name not in super().__getattribute__(_GuiState.__attrs[1]):
             raise AttributeError(f"Variable '{name}' is not defined.")
         with self._notebook_context(gui), self._set_context(gui):
             encoded_name = gui._bind_var(name)
             return getattr(gui._bindings(), encoded_name)
 
     def __setattr__(self, name: str, value: t.Any) -> None:
-        gui: "Gui" = super().__getattribute__(State.__gui_attr)
+        gui: "Gui" = super().__getattribute__(_GuiState.__gui_attr)
         if gui._is_in_brdcst_callback() and (
             name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
         ):
             raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
-        if not name.startswith("__") and name not in super().__getattribute__(State.__attrs[1]):
+        if not name.startswith("__") and name not in super().__getattribute__(_GuiState.__attrs[1]):
             raise AttributeError(f"Variable '{name}' is not accessible.")
         with self._notebook_context(gui), self._set_context(gui):
             encoded_name = gui._bind_var(name)
             setattr(gui._bindings(), encoded_name, value)
 
     def __getitem__(self, key: str):
-        context = key if key in super().__getattribute__(State.__attrs[2]) else None
+        context = key if key in super().__getattribute__(_GuiState.__attrs[2]) else None
         if context is None:
-            gui: "Gui" = super().__getattribute__(State.__gui_attr)
+            gui: "Gui" = super().__getattribute__(_GuiState.__gui_attr)
             page_ctx = gui._get_page_context(key)
             context = page_ctx if page_ctx is not None else None
         if context is None:
             raise RuntimeError(f"Can't resolve context '{key}' from state object")
-        self._set_placeholder(State.__placeholder_attrs[1], context)
+        self._set_placeholder(_GuiState.__placeholder_attrs[1], context)
         return self
 
     def _set_context(self, gui: "Gui") -> t.ContextManager[None]:
-        if (pl_ctx := self._get_placeholder(State.__placeholder_attrs[1])) is not None:
-            self._set_placeholder(State.__placeholder_attrs[1], None)
+        if (pl_ctx := self._get_placeholder(_GuiState.__placeholder_attrs[1])) is not None:
+            self._set_placeholder(_GuiState.__placeholder_attrs[1], None)
             if pl_ctx != gui._get_locals_context():
                 return gui._set_locals_context(pl_ctx)
         if len(inspect.stack()) > 1:
@@ -176,7 +256,7 @@ class State:
         return gui.get_flask_app().app_context() if not has_app_context() and _is_in_notebook() else nullcontext()
 
     def _get_placeholder(self, name: str):
-        if name in State.__placeholder_attrs:
+        if name in _GuiState.__placeholder_attrs:
             try:
                 return super().__getattribute__(name)
             except AttributeError:
@@ -184,81 +264,16 @@ class State:
         return None
 
     def _set_placeholder(self, name: str, value: t.Any):
-        if name in State.__placeholder_attrs:
+        if name in _GuiState.__placeholder_attrs:
             super().__setattr__(name, value)
 
     def _get_placeholder_attrs(self):
-        return State.__placeholder_attrs
+        return _GuiState.__placeholder_attrs
 
     def _add_attribute(self, name: str, default_value: t.Optional[t.Any] = None) -> bool:
-        attrs: t.List[str] = super().__getattribute__(State.__attrs[1])
+        attrs: t.List[str] = super().__getattribute__(_GuiState.__attrs[1])
         if name not in attrs:
             attrs.append(name)
-            gui = super().__getattribute__(State.__gui_attr)
+            gui = super().__getattribute__(_GuiState.__gui_attr)
             return gui._bind_var_val(name, default_value)
         return False
-
-    def assign(self, name: str, value: t.Any) -> t.Any:
-        """Assign a value to a state variable.
-
-        This should be used only from within a lambda function used
-        as a callback in a visual element.
-
-        Arguments:
-            name (str): The variable name to assign to.
-            value (Any): The new variable value.
-
-        Returns:
-            Any: The previous value of the variable.
-        """
-        val = attrgetter(name)(self)
-        _attrsetter(self, name, value)
-        return val
-
-    def refresh(self, name: str):
-        """Refresh a state variable.
-
-        This allows to re-sync the user interface with a variable value.
-
-        Arguments:
-            name (str): The variable name to refresh.
-        """
-        val = attrgetter(name)(self)
-        _attrsetter(self, name, val)
-
-    def broadcast(self, name: str, value: t.Any):
-        """Update a variable on all clients.
-
-        All connected clients will receive an update of the variable called *name* with the
-        provided value, even if it is not shared.
-
-        Arguments:
-            name (str): The variable name to update.
-            value (Any): The new variable value.
-        """
-        gui: "Gui" = super().__getattribute__(State.__gui_attr)
-        with self._set_context(gui):
-            encoded_name = gui._bind_var(name)
-            gui._broadcast_all_clients(encoded_name, value)
-
-    def __enter__(self):
-        super().__getattribute__(State.__attrs[0]).__enter__()
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        return super().__getattribute__(State.__attrs[0]).__exit__(exc_type, exc_value, traceback)
-
-    def set_favicon(self, favicon_path: t.Union[str, Path]):
-        """Change the favicon for the client of this state.
-
-        This function dynamically changes the favicon (the icon associated with the application's
-        pages) of Taipy GUI pages for the specific client of this state.
-
-        Note that the *favicon* parameter to `(Gui.)run()^` can also be used to change
-        the favicon when the application starts.
-
-        Arguments:
-            favicon_path: The path to the image file to use.<br/>
-                This can be expressed as a path name or a URL (relative or not).
-        """
-        super().__getattribute__(State.__gui_attr).set_favicon(favicon_path, self)

+ 6 - 7
taipy/gui/viselements.json

@@ -1736,13 +1736,6 @@
                         "type": "dynamic(list[str])",
                         "doc": "The list of messages. Each item of this list must consist of a list of three strings: a message identifier, a message content, a user identifier, and an image URL."
                     },
-                    {
-                        "name": "max_file_size",
-                        "type": "int",
-                        "default_value": "1 * 1024 * 1024 (1MB)",
-                        "doc": "The maximum file size can be uploaded to a chat message."
-
-                    },
                     {
                         "name": "users",
                         "type": "dynamic(list[Union[str,Icon]])",
@@ -1801,6 +1794,12 @@
                         "type": "str",
                         "default_value": "\"markdown\"",
                         "doc": "Define the way the messages are processed when they are displayed:\n<ul><li>&quot;raw&quot; no processing</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown.</li></ul>"
+                    },
+                    {
+                        "name": "max_file_size",
+                        "type": "int",
+                        "default_value": "1024 * 1024",
+                        "doc": "The maximum allowable file size, in bytes, for files uploaded to a chat message.\nThe default is 1 MB."
                     }
                 ]
             }

+ 7 - 5
taipy/gui_core/_context.py

@@ -516,7 +516,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
             finally:
                 self.scenario_refresh(scenario_id)
                 if (scenario or user_scenario) and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
-                    self.gui._update_var(sel_scenario_var, scenario or user_scenario, on_change=args[2])
+                    self.gui._update_var(
+                        sel_scenario_var[6:] if sel_scenario_var.startswith("_TpLv_") else sel_scenario_var,
+                        scenario or user_scenario,
+                        on_change=args[2],
+                    )
         if scenario:
             if not (reason := is_editable(scenario)):
                 state.assign(error_var, f"Scenario {scenario_id or name} is not editable: {_get_reason(reason)}.")
@@ -1124,10 +1128,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
             try:
                 return [
-                        (k, f"{v}")
-                        for k, v in dn._get_user_properties().items()
-                        if k != _GuiCoreContext.__PROP_ENTITY_NAME
-                    ]
+                    (k, f"{v}") for k, v in dn._get_user_properties().items() if k != _GuiCoreContext.__PROP_ENTITY_NAME
+                ]
             except Exception:
                 return None
         return None

+ 3 - 2
tests/gui/actions/test_download.py

@@ -10,8 +10,9 @@
 # specific language governing permissions and limitations under the License.
 
 import inspect
+import typing as t
 
-from flask import g
+from flask import Flask, g
 
 from taipy.gui import Gui, Markdown, State, download
 
@@ -30,7 +31,7 @@ def test_download(gui: Gui, helpers):
     gui.run(run_server=False)
     flask_client = gui._server.test_client()
     # WS client and emit
-    ws_client = gui._server._ws.test_client(gui._server.get_flask())
+    ws_client = gui._server._ws.test_client(t.cast(Flask, gui._server.get_flask()))
     cid = helpers.create_scope_and_get_sid(gui)
     # Get the jsx once so that the page will be evaluated -> variable will be registered
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")

+ 38 - 0
tests/gui/builder/control/test_metric.py

@@ -0,0 +1,38 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# 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 taipy.gui.builder as tgb
+from taipy.gui import Gui
+
+
+def test_metric_builder_none(gui: Gui, helpers):
+    with tgb.Page(frame=None) as page:
+        tgb.metric(type=None, value=42)
+    expected_list = ["<Metric", 'type="None"', 'value={42.0}']
+    helpers.test_control_builder(gui, page, expected_list)
+
+def test_metric_builder_none_lowercase(gui: Gui, helpers):
+    with tgb.Page(frame=None) as page:
+        tgb.metric(type="none", value=42)
+    expected_list = ["<Metric", 'type="none"', 'value={42.0}']
+    helpers.test_control_builder(gui, page, expected_list)
+
+def test_metric_builder_circular(gui: Gui, helpers):
+    with tgb.Page(frame=None) as page:
+        tgb.metric(type="circular", value=42)
+    expected_list = ["<Metric", 'type="circular"', 'value={42.0}']
+    helpers.test_control_builder(gui, page, expected_list)
+
+def test_metric_builder_linear(gui: Gui, helpers):
+    with tgb.Page(frame=None) as page:
+        tgb.metric(type="linear", value=42)
+    expected_list = ["<Metric", 'type="linear"', 'value={42.0}']
+    helpers.test_control_builder(gui, page, expected_list)

+ 53 - 0
tests/gui/control/test_metric.py

@@ -0,0 +1,53 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# 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.
+
+from taipy.gui import Gui
+
+
+def test_metric_md_none(gui: Gui, helpers):
+    md_string = "<|metric|type=None|value=42|>"
+    expected_list = ["<Metric", 'type="None"', 'value={42.0}']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+def test_metric_md_none_lowercase(gui: Gui, helpers):
+    md_string = "<|metric|type=none|value=42|>"
+    expected_list = ["<Metric", 'type="none"', 'value={42.0}']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+def test_metric_md_circular(gui: Gui, helpers):
+    md_string = "<|metric|type=circular|value=42|>"
+    expected_list = ["<Metric", 'type="circular"', 'value={42.0}']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+def test_metric_md_linear(gui: Gui, helpers):
+    md_string = "<|metric|type=linear|value=42|>"
+    expected_list = ["<Metric", 'type="linear"', 'value={42.0}']
+    helpers.test_control_md(gui, md_string, expected_list)
+
+def test_metric_html_none(gui: Gui, helpers):
+    html_string = '<taipy:metric type="None" value="42" />'
+    expected_list = ["<Metric", 'type="None"', 'value={42.0}']
+    helpers.test_control_html(gui, html_string, expected_list)
+
+def test_metric_html_none_lowercase(gui: Gui, helpers):
+    html_string = '<taipy:metric type="none" value="42" />'
+    expected_list = ["<Metric", 'type="none"', 'value={42.0}']
+    helpers.test_control_html(gui, html_string, expected_list)
+
+def test_metric_html_circular(gui: Gui, helpers):
+    html_string = '<taipy:metric type="circular" value="42" />'
+    expected_list = ["<Metric", 'type="circular"', 'value={42.0}']
+    helpers.test_control_html(gui, html_string, expected_list)
+
+def test_metric_html_linear(gui: Gui, helpers):
+    html_string = '<taipy:metric type="linear" value="42" />'
+    expected_list = ["<Metric", 'type="linear"', 'value={42.0}']
+    helpers.test_control_html(gui, html_string, expected_list)

+ 106 - 0
tests/gui/mock/test_mock_state.py

@@ -0,0 +1,106 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# 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.
+
+from unittest.mock import Mock
+
+from taipy.gui import Gui, State
+from taipy.gui.mock.mock_state import MockState
+from taipy.gui.utils import _MapDict
+
+
+def test_gui():
+    gui = Gui("")
+    ms = MockState(gui)
+    assert ms.get_gui() is gui
+    assert ms._gui is gui
+
+
+def test_read_attr():
+    gui = Gui("")
+    ms = MockState(gui, a=1)
+    assert ms is not None
+    assert ms.a == 1
+    assert ms.b is None
+
+
+def test_read_context():
+    ms = MockState(Gui(""), a=1)
+    assert ms["b"] is not None
+    assert ms["b"].a == 1
+
+
+def test_write_attr():
+    ms = MockState(Gui(""), a=1)
+    ms.a = 2
+    assert ms.a == 2
+    ms.b = 3
+    assert ms.b == 3
+    ms.a += 1
+    assert ms.a == 3
+
+def test_dict():
+    ms = MockState(Gui(""))
+    a_dict = {"a": 1}
+    ms.d = a_dict
+    assert isinstance(ms.d, _MapDict)
+    assert ms.d._dict is a_dict
+
+
+def test_write_context():
+    ms = MockState(Gui(""), a=1)
+    ms["page"].a = 2
+    assert ms["page"].a == 2
+    ms["page"].b = 3
+    assert ms["page"].b == 3
+
+def test_assign():
+    ms = MockState(Gui(""), a=1)
+    ms.assign("a", 2)
+    assert ms.a == 2
+    ms.assign("b", 1)
+    assert ms.b == 1
+
+def test_refresh():
+    ms = MockState(Gui(""), a=1)
+    ms.refresh("a")
+    assert ms.a == 1
+    ms.a = 2
+    ms.refresh("a")
+    assert ms.a == 2
+
+def test_context_manager():
+    with MockState(Gui(""), a=1) as ms:
+        assert ms is not None
+        ms.a = 2
+    assert ms.a == 2
+
+def test_broadcast():
+    ms = MockState(Gui(""), a=1)
+    ms.broadcast("a", 2)
+
+def test_set_favicon():
+    gui = Gui("")
+    gui.set_favicon = Mock()
+    ms = MockState(gui, a=1)
+    ms.set_favicon("a_path")
+    gui.set_favicon.assert_called_once()
+
+def test_callback():
+    def on_action(state: State):
+        state.assign("a", 2)
+
+    ms = MockState(Gui(""), a=1)
+    on_action(ms)
+    assert ms.a == 2
+
+def test_false():
+    ms = MockState(Gui(""), a=False)
+    assert ms.a is False