Repository: robocorp/rpaframework

Issue:
RPA.Dialogs date picker
Add the possibility of picking up a date from the dialog UI

- [x] Support internally a date picking React element (in **robocorp-dialog** dependency)
- [x] `Add Date Input` keyword in RF displaying and collecting data from the element

### Limitations
- Can't easily pick a date far away from the current year (or provided default)
- Can't input date as text directly in the entry (read-only value property)


Discussion:

Resolving patch:
diff --git a/packages/dialogs/src/RPA/Dialogs/dialog.py b/packages/dialogs/src/RPA/Dialogs/dialog.py
--- a/packages/dialogs/src/RPA/Dialogs/dialog.py
+++ b/packages/dialogs/src/RPA/Dialogs/dialog.py
@@ -2,11 +2,12 @@
 import logging
 import subprocess
 import time
-from typing import Union, Optional
+from datetime import datetime
+from typing import Any, Union, Optional
 
 import robocorp_dialog  # type: ignore
 from robot.utils import DotDict  # type: ignore
-from .dialog_types import Elements, Result
+from .dialog_types import Element, Elements, Result
 from .utils import is_input, is_submit
 
 
@@ -17,6 +18,13 @@ class TimeoutException(RuntimeError):
 class Dialog:
     """Container for a dialog running in a separate subprocess."""
 
+    # Post-process received result's values.
+    POST_PROCESS_MAP = {
+        "input-datepicker": lambda value, *, _format, **__: datetime.strptime(
+            value, _format  # converts date strings to objects
+        ).date(),
+    }
+
     def __init__(
         self,
         elements: Elements,
@@ -144,7 +152,7 @@ def result(self) -> Result:
             if is_input(element):
                 key = element["name"]
                 assert key in fields, f"Missing input value for '{key}'"
-                result[key] = fields[key]
+                result[key] = self._post_process_value(fields[key], element=element)
             elif is_submit(element):
                 result["submit"] = fields["submit"]
 
@@ -187,3 +195,11 @@ def _to_result(self, stdout: bytes, stderr: bytes) -> None:
             self._result = json.loads(out)
         except ValueError:
             self._result = {"error": f"Malformed response:\n{out}"}
+
+    @classmethod
+    def _post_process_value(cls, value: Any, *, element: Element) -> Any:
+        func = cls.POST_PROCESS_MAP.get(element["type"])
+        if func:
+            value = func(value, **element)
+
+        return value
diff --git a/packages/dialogs/src/RPA/Dialogs/library.py b/packages/dialogs/src/RPA/Dialogs/library.py
--- a/packages/dialogs/src/RPA/Dialogs/library.py
+++ b/packages/dialogs/src/RPA/Dialogs/library.py
@@ -4,6 +4,7 @@
 import logging
 import mimetypes
 import time
+from datetime import date, datetime
 from pathlib import Path
 from typing import Dict, List, Optional, Union, Any, Generator
 
@@ -696,6 +697,55 @@ def add_drop_down(
 
         self.add_element(element)
 
+    @keyword("Add Date Input", tags=["input"])
+    def add_date_input(
+        self,
+        name: str,
+        default: Optional[Union[date, str]] = None,
+    ) -> None:
+        """Add a date input element
+
+        :param name:    Name of result field
+        :param default: The default date
+
+        Displays a date input widget. The selection the user makes will be available
+        as a ``date`` object in the ``name`` field of the result.
+        The ``default`` argument can be a pre-set date as object or string in
+        DD/MM/YYYY format, otherwise the current date is used.
+
+        Example:
+
+        .. code-block:: robotframework
+
+            Add heading       Enter your birthdate
+            Add Date Input    birthdate    default=26/04/1993
+            ${result} =       Run dialog
+            Log To Console    User birthdate year should be: ${result.birthdate.year}
+        """
+
+        # TODO(cmin764): Be flexible on date formats. (provide it as parameter)
+        py_date_format = "%d/%m/%Y"
+        js_date_format = "dd/MM/yyyy"
+        default = default or datetime.utcnow().date()
+        if isinstance(default, date):  # recognizes both `date` and `datetime`
+            default = default.strftime(py_date_format)
+        else:
+            try:
+                datetime.strptime(default, py_date_format)
+            except Exception as exc:
+                raise ValueError(
+                    f"Invalid default date with value {default!r}"
+                ) from exc
+
+        element = {
+            "type": "input-datepicker",
+            "name": str(name),
+            "_format": py_date_format,
+            "format": js_date_format,
+            "default": optional_str(default),
+        }
+        self.add_element(element)
+
     @keyword("Add radio buttons", tags=["input"])
     def add_radio_buttons(
         self,


Test patch:
diff --git a/packages/dialogs/tests/test_dialogs.robot b/packages/dialogs/tests/test_dialogs.robot
--- a/packages/dialogs/tests/test_dialogs.robot
+++ b/packages/dialogs/tests/test_dialogs.robot
@@ -1,4 +1,5 @@
 *** Settings ***
+Library    DateTime
 Library    RPA.Dialogs
 
 *** Variables ***
@@ -53,6 +54,11 @@ All elements
     Add drop-down       dropdown-field-2   one,two,three    default=three
     Add drop-down       dropdown-field-3   ${EMPTY_LIST}    label=Empty
 
+    Add Date Input      dateinput-field-1
+    Add Date Input      dateinput-field-2   default=06/09/2021
+    ${date_obj} =       Convert Date    26/04/1993    date_format=%d/%m/%Y    result_format=datetime
+    Add Date Input      dateinput-field-3   default=${date_obj}
+
     Add radio buttons   radio-field-1      one,two,three
     Add radio buttons   radio-field-2      one,two,three    default=three
     Add radio buttons   radio-field-3      ${EMPTY_LIST}    label=Empty
diff --git a/packages/dialogs/tests/test_elements.py b/packages/dialogs/tests/test_elements.py
--- a/packages/dialogs/tests/test_elements.py
+++ b/packages/dialogs/tests/test_elements.py
@@ -1,5 +1,11 @@
-import pytest
+try:
+    from contextlib import nullcontext as does_not_raise
+except ImportError:
+    from contextlib import ExitStack as does_not_raise
+from datetime import date, datetime
 from pathlib import Path
+
+import pytest
 from RPA.Dialogs import Dialogs
 
 
@@ -157,6 +163,40 @@ def test_add_drop_down(library):
     }
 
 
+@pytest.mark.parametrize(
+    "default,expect",
+    [
+        ("06/09/2021", does_not_raise()),
+        ("33/09/2021", pytest.raises(ValueError)),
+        ("06/13/2021", pytest.raises(ValueError)),
+        (None, does_not_raise()),  # uses the current date as default
+        (datetime.utcnow(), does_not_raise()),
+        (datetime.utcnow().date(), does_not_raise()),
+        (True, pytest.raises(ValueError)),  # unexpected value
+    ]
+)
+@pytest.mark.freeze_time("2021-09-06")
+def test_add_date_input(library, default, expect):
+    with expect:
+        library.add_date_input("datepicker-field", default=default)
+
+    py_date_format = "%d/%m/%Y"
+    default = default or "06/09/2021"  # for the current date case
+    if isinstance(default, date):
+        default = default.strftime(py_date_format)
+
+    elems_count = len(library.elements)
+    assert elems_count in (0, 1)
+    if elems_count:
+        assert library.elements[0] == {
+            "type": "input-datepicker",
+            "name": "datepicker-field",
+            "_format": py_date_format,
+            "format": "dd/MM/yyyy",
+            "default": default,
+        }
+
+
 def test_add_radio_buttons(library):
     library.add_radio_buttons("radio-field", options=[])
     assert len(library.elements) == 1