test_foreach.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. from typing import Dict, List, Set, Tuple, Union
  2. import pydantic.v1
  3. import pytest
  4. from reflex import el
  5. from reflex.base import Base
  6. from reflex.components.component import Component
  7. from reflex.components.core.foreach import (
  8. Foreach,
  9. ForeachRenderError,
  10. ForeachVarError,
  11. foreach,
  12. )
  13. from reflex.components.radix.themes.layout.box import box
  14. from reflex.components.radix.themes.typography.text import text
  15. from reflex.state import BaseState, ComponentState
  16. from reflex.vars.base import Var
  17. from reflex.vars.number import NumberVar
  18. from reflex.vars.sequence import ArrayVar
  19. class ForEachTag(Base):
  20. """A tag for testing the ForEach component."""
  21. name: str = ""
  22. class ForEachState(BaseState):
  23. """A state for testing the ForEach component."""
  24. colors_list: List[str] = ["red", "yellow"]
  25. nested_colors_list: List[List[str]] = [["red", "yellow"], ["blue", "green"]]
  26. colors_dict_list: List[Dict[str, str]] = [
  27. {
  28. "name": "red",
  29. },
  30. {"name": "yellow"},
  31. ]
  32. colors_nested_dict_list: List[Dict[str, List[str]]] = [{"shades": ["light-red"]}]
  33. primary_color: Dict[str, str] = {"category": "primary", "name": "red"}
  34. color_with_shades: Dict[str, List[str]] = {
  35. "red": ["orange", "yellow"],
  36. "yellow": ["orange", "green"],
  37. }
  38. nested_colors_with_shades: Dict[str, Dict[str, List[Dict[str, str]]]] = {
  39. "primary": {"red": [{"shade": "dark"}]}
  40. }
  41. color_tuple: Tuple[str, str] = (
  42. "red",
  43. "yellow",
  44. )
  45. colors_set: Set[str] = {"red", "green"}
  46. bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]]
  47. color_index_tuple: Tuple[int, str] = (0, "red")
  48. default_factory_list: list[ForEachTag] = pydantic.v1.Field(default_factory=list)
  49. class ComponentStateTest(ComponentState):
  50. """A test component state."""
  51. foo: bool
  52. @classmethod
  53. def get_component(cls, *children, **props) -> Component:
  54. """Get the component.
  55. Args:
  56. children: The children components.
  57. props: The component props.
  58. Returns:
  59. The component.
  60. """
  61. return el.div(*children, **props)
  62. def display_color(color):
  63. assert color._var_type is str
  64. return box(text(color))
  65. def display_color_name(color):
  66. assert color._var_type == Dict[str, str]
  67. return box(text(color["name"]))
  68. def display_shade(color):
  69. assert color._var_type == Dict[str, List[str]]
  70. return box(text(color["shades"][0]))
  71. def display_primary_colors(color):
  72. assert color._var_type == Tuple[str, str]
  73. return box(text(color[0]), text(color[1]))
  74. def display_color_with_shades(color):
  75. assert color._var_type == Tuple[str, List[str]]
  76. return box(text(color[0]), text(color[1][0]))
  77. def display_nested_color_with_shades(color):
  78. assert color._var_type == Tuple[str, Dict[str, List[Dict[str, str]]]]
  79. return box(text(color[0]), text(color[1]["red"][0]["shade"]))
  80. def show_shade(item):
  81. return text(item[1][0]["shade"])
  82. def display_nested_color_with_shades_v2(color):
  83. assert color._var_type == Tuple[str, Dict[str, List[Dict[str, str]]]]
  84. return box(text(foreach(color[1], show_shade)))
  85. def display_color_tuple(color):
  86. assert color._var_type is str
  87. return box(text(color))
  88. def display_colors_set(color):
  89. assert color._var_type is str
  90. return box(text(color))
  91. def display_nested_list_element(element: ArrayVar[List[str]], index: NumberVar[int]):
  92. assert element._var_type == List[str]
  93. assert index._var_type is int
  94. return box(text(element[index]))
  95. def display_color_index_tuple(color):
  96. assert color._var_type == Union[int, str]
  97. return box(text(color))
  98. seen_index_vars = set()
  99. @pytest.mark.parametrize(
  100. "state_var, render_fn, render_dict",
  101. [
  102. (
  103. ForEachState.colors_list,
  104. display_color,
  105. {
  106. "iterable_state": f"{ForEachState.get_full_name()}.colors_list",
  107. "iterable_type": "list",
  108. },
  109. ),
  110. (
  111. ForEachState.colors_dict_list,
  112. display_color_name,
  113. {
  114. "iterable_state": f"{ForEachState.get_full_name()}.colors_dict_list",
  115. "iterable_type": "list",
  116. },
  117. ),
  118. (
  119. ForEachState.colors_nested_dict_list,
  120. display_shade,
  121. {
  122. "iterable_state": f"{ForEachState.get_full_name()}.colors_nested_dict_list",
  123. "iterable_type": "list",
  124. },
  125. ),
  126. (
  127. ForEachState.primary_color,
  128. display_primary_colors,
  129. {
  130. "iterable_state": f"{ForEachState.get_full_name()}.primary_color",
  131. "iterable_type": "dict",
  132. },
  133. ),
  134. (
  135. ForEachState.color_with_shades,
  136. display_color_with_shades,
  137. {
  138. "iterable_state": f"{ForEachState.get_full_name()}.color_with_shades",
  139. "iterable_type": "dict",
  140. },
  141. ),
  142. (
  143. ForEachState.nested_colors_with_shades,
  144. display_nested_color_with_shades,
  145. {
  146. "iterable_state": f"{ForEachState.get_full_name()}.nested_colors_with_shades",
  147. "iterable_type": "dict",
  148. },
  149. ),
  150. (
  151. ForEachState.nested_colors_with_shades,
  152. display_nested_color_with_shades_v2,
  153. {
  154. "iterable_state": f"{ForEachState.get_full_name()}.nested_colors_with_shades",
  155. "iterable_type": "dict",
  156. },
  157. ),
  158. (
  159. ForEachState.color_tuple,
  160. display_color_tuple,
  161. {
  162. "iterable_state": f"{ForEachState.get_full_name()}.color_tuple",
  163. "iterable_type": "tuple",
  164. },
  165. ),
  166. (
  167. ForEachState.colors_set,
  168. display_colors_set,
  169. {
  170. "iterable_state": f"{ForEachState.get_full_name()}.colors_set",
  171. "iterable_type": "set",
  172. },
  173. ),
  174. (
  175. ForEachState.nested_colors_list,
  176. lambda el, i: display_nested_list_element(el, i),
  177. {
  178. "iterable_state": f"{ForEachState.get_full_name()}.nested_colors_list",
  179. "iterable_type": "list",
  180. },
  181. ),
  182. (
  183. ForEachState.color_index_tuple,
  184. display_color_index_tuple,
  185. {
  186. "iterable_state": f"{ForEachState.get_full_name()}.color_index_tuple",
  187. "iterable_type": "tuple",
  188. },
  189. ),
  190. ],
  191. )
  192. def test_foreach_render(state_var, render_fn, render_dict):
  193. """Test that the foreach component renders without error.
  194. Args:
  195. state_var: the state var.
  196. render_fn: The render callable
  197. render_dict: return dict on calling `component.render`
  198. """
  199. component = Foreach.create(state_var, render_fn)
  200. rend = component.render()
  201. assert rend["iterable_state"] == render_dict["iterable_state"]
  202. assert rend["iterable_type"] == render_dict["iterable_type"]
  203. # Make sure the index vars are unique.
  204. arg_index = rend["arg_index"]
  205. assert isinstance(arg_index, Var)
  206. assert arg_index._js_expr not in seen_index_vars
  207. assert arg_index._var_type is int
  208. seen_index_vars.add(arg_index._js_expr)
  209. def test_foreach_bad_annotations():
  210. """Test that the foreach component raises a ForeachVarError if the iterable is of type Any."""
  211. with pytest.raises(ForeachVarError):
  212. Foreach.create(
  213. ForEachState.bad_annotation_list,
  214. lambda sublist: Foreach.create(sublist, lambda color: text(color)),
  215. )
  216. def test_foreach_no_param_in_signature():
  217. """Test that the foreach component raises a ForeachRenderError if no parameters are passed."""
  218. with pytest.raises(ForeachRenderError):
  219. Foreach.create(
  220. ForEachState.colors_list,
  221. lambda: text("color"),
  222. )
  223. def test_foreach_too_many_params_in_signature():
  224. """Test that the foreach component raises a ForeachRenderError if too many parameters are passed."""
  225. with pytest.raises(ForeachRenderError):
  226. Foreach.create(
  227. ForEachState.colors_list,
  228. lambda color, index, extra: text(color),
  229. )
  230. def test_foreach_component_styles():
  231. """Test that the foreach component works with global component styles."""
  232. component = el.div(
  233. foreach(
  234. ForEachState.colors_list,
  235. display_color,
  236. )
  237. )
  238. component._add_style_recursive({box: {"color": "red"}})
  239. assert 'css={({ ["color"] : "red" })}' in str(component)
  240. def test_foreach_component_state():
  241. """Test that using a component state to render in the foreach raises an error."""
  242. with pytest.raises(TypeError):
  243. Foreach.create(
  244. ForEachState.colors_list,
  245. ComponentStateTest.create,
  246. )
  247. def test_foreach_default_factory():
  248. """Test that the default factory is called."""
  249. _ = Foreach.create(
  250. ForEachState.default_factory_list,
  251. lambda tag: text(tag.name),
  252. )