test_style.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. from __future__ import annotations
  2. from typing import Any, Mapping
  3. import pytest
  4. import reflex as rx
  5. from reflex import style
  6. from reflex.components.component import evaluate_style_namespaces
  7. from reflex.style import Style
  8. from reflex.vars import VarData
  9. from reflex.vars.base import LiteralVar, Var
  10. test_style = [
  11. ({"a": 1}, {"a": 1}),
  12. ({"a": LiteralVar.create("abc")}, {"a": "abc"}),
  13. ({"test_case": 1}, {"testCase": 1}),
  14. ({"test_case": {"a": 1}}, {"test_case": {"a": 1}}),
  15. ({":test_case": {"a": 1}}, {":test_case": {"a": 1}}),
  16. ({"::test_case": {"a": 1}}, {"::test_case": {"a": 1}}),
  17. (
  18. {"::-webkit-scrollbar": {"display": "none"}},
  19. {"::-webkit-scrollbar": {"display": "none"}},
  20. ),
  21. ({"margin_y": "2rem"}, {"marginTop": "2rem", "marginBottom": "2rem"}),
  22. ({"marginY": "2rem"}, {"marginTop": "2rem", "marginBottom": "2rem"}),
  23. (
  24. {"::-webkit-scrollbar": {"bgColor": "red"}},
  25. {"::-webkit-scrollbar": {"backgroundColor": "red"}},
  26. ),
  27. (
  28. {"paddingX": ["2rem", "3rem"]},
  29. {
  30. "paddingInlineStart": ["2rem", "3rem"],
  31. "paddingInlineEnd": ["2rem", "3rem"],
  32. },
  33. ),
  34. ]
  35. @pytest.mark.parametrize(
  36. "style_dict,expected",
  37. test_style,
  38. )
  39. def test_convert(style_dict, expected):
  40. """Test Format a style dictionary.
  41. Args:
  42. style_dict: The style to check.
  43. expected: The expected formatted style.
  44. """
  45. converted_dict, _var_data = style.convert(style_dict)
  46. assert LiteralVar.create(converted_dict).equals(LiteralVar.create(expected))
  47. @pytest.mark.parametrize(
  48. "style_dict,expected",
  49. test_style,
  50. )
  51. def test_create_style(style_dict, expected):
  52. """Test style dictionary.
  53. Args:
  54. style_dict: The style to check.
  55. expected: The expected formatted style.
  56. """
  57. assert LiteralVar.create(style.Style(style_dict)).equals(
  58. LiteralVar.create(expected)
  59. )
  60. def compare_dict_of_var(d1: dict[str, Any], d2: dict[str, Any]):
  61. """Compare two dictionaries of Var objects.
  62. Args:
  63. d1: The first dictionary.
  64. d2: The second dictionary.
  65. """
  66. assert len(d1) == len(d2)
  67. for key, value in d1.items():
  68. assert key in d2
  69. if isinstance(value, dict):
  70. compare_dict_of_var(value, d2[key])
  71. elif isinstance(value, Var):
  72. assert value.equals(d2[key])
  73. else:
  74. assert value == d2[key]
  75. @pytest.mark.parametrize(
  76. ("kwargs", "style_dict", "expected_get_style"),
  77. [
  78. ({}, {}, {}),
  79. (
  80. {"color": "hotpink"},
  81. {},
  82. {"css": LiteralVar.create(Style({"color": "hotpink"}))},
  83. ),
  84. ({}, {"color": "red"}, {"css": LiteralVar.create(Style({"color": "red"}))}),
  85. (
  86. {"color": "hotpink"},
  87. {"color": "red"},
  88. {"css": LiteralVar.create(Style({"color": "hotpink"}))},
  89. ),
  90. (
  91. {"_hover": {"color": "hotpink"}},
  92. {},
  93. {"css": LiteralVar.create(Style({"&:hover": {"color": "hotpink"}}))},
  94. ),
  95. (
  96. {},
  97. {"_hover": {"color": "red"}},
  98. {"css": LiteralVar.create(Style({"&:hover": {"color": "red"}}))},
  99. ),
  100. (
  101. {},
  102. {":hover": {"color": "red"}},
  103. {"css": LiteralVar.create(Style({"&:hover": {"color": "red"}}))},
  104. ),
  105. (
  106. {},
  107. {"::-webkit-scrollbar": {"display": "none"}},
  108. {
  109. "css": LiteralVar.create(
  110. Style({"&::-webkit-scrollbar": {"display": "none"}})
  111. )
  112. },
  113. ),
  114. (
  115. {},
  116. {"::-moz-progress-bar": {"background_color": "red"}},
  117. {
  118. "css": LiteralVar.create(
  119. Style({"&::-moz-progress-bar": {"backgroundColor": "red"}})
  120. )
  121. },
  122. ),
  123. (
  124. {"color": ["#111", "#222", "#333", "#444", "#555"]},
  125. {},
  126. {
  127. "css": LiteralVar.create(
  128. Style(
  129. {
  130. "@media screen and (min-width: 0)": {"color": "#111"},
  131. "@media screen and (min-width: 30em)": {"color": "#222"},
  132. "@media screen and (min-width: 48em)": {"color": "#333"},
  133. "@media screen and (min-width: 62em)": {"color": "#444"},
  134. "@media screen and (min-width: 80em)": {"color": "#555"},
  135. }
  136. )
  137. )
  138. },
  139. ),
  140. (
  141. {
  142. "color": ["#111", "#222", "#333", "#444", "#555"],
  143. "background_color": "#FFF",
  144. },
  145. {},
  146. {
  147. "css": LiteralVar.create(
  148. Style(
  149. {
  150. "@media screen and (min-width: 0)": {"color": "#111"},
  151. "@media screen and (min-width: 30em)": {"color": "#222"},
  152. "@media screen and (min-width: 48em)": {"color": "#333"},
  153. "@media screen and (min-width: 62em)": {"color": "#444"},
  154. "@media screen and (min-width: 80em)": {"color": "#555"},
  155. "backgroundColor": "#FFF",
  156. }
  157. )
  158. )
  159. },
  160. ),
  161. (
  162. {
  163. "color": ["#111", "#222", "#333", "#444", "#555"],
  164. "background_color": ["#FFF", "#EEE", "#DDD", "#CCC", "#BBB"],
  165. },
  166. {},
  167. {
  168. "css": LiteralVar.create(
  169. Style(
  170. {
  171. "@media screen and (min-width: 0)": {
  172. "color": "#111",
  173. "backgroundColor": "#FFF",
  174. },
  175. "@media screen and (min-width: 30em)": {
  176. "color": "#222",
  177. "backgroundColor": "#EEE",
  178. },
  179. "@media screen and (min-width: 48em)": {
  180. "color": "#333",
  181. "backgroundColor": "#DDD",
  182. },
  183. "@media screen and (min-width: 62em)": {
  184. "color": "#444",
  185. "backgroundColor": "#CCC",
  186. },
  187. "@media screen and (min-width: 80em)": {
  188. "color": "#555",
  189. "backgroundColor": "#BBB",
  190. },
  191. }
  192. )
  193. )
  194. },
  195. ),
  196. (
  197. {
  198. "_hover": [
  199. {"color": "#111"},
  200. {"color": "#222"},
  201. {"color": "#333"},
  202. {"color": "#444"},
  203. {"color": "#555"},
  204. ]
  205. },
  206. {},
  207. {
  208. "css": LiteralVar.create(
  209. Style(
  210. {
  211. "&:hover": {
  212. "@media screen and (min-width: 0)": {"color": "#111"},
  213. "@media screen and (min-width: 30em)": {
  214. "color": "#222"
  215. },
  216. "@media screen and (min-width: 48em)": {
  217. "color": "#333"
  218. },
  219. "@media screen and (min-width: 62em)": {
  220. "color": "#444"
  221. },
  222. "@media screen and (min-width: 80em)": {
  223. "color": "#555"
  224. },
  225. }
  226. }
  227. )
  228. )
  229. },
  230. ),
  231. (
  232. {"_hover": {"color": ["#111", "#222", "#333", "#444", "#555"]}},
  233. {},
  234. {
  235. "css": LiteralVar.create(
  236. Style(
  237. {
  238. "&:hover": {
  239. "@media screen and (min-width: 0)": {"color": "#111"},
  240. "@media screen and (min-width: 30em)": {
  241. "color": "#222"
  242. },
  243. "@media screen and (min-width: 48em)": {
  244. "color": "#333"
  245. },
  246. "@media screen and (min-width: 62em)": {
  247. "color": "#444"
  248. },
  249. "@media screen and (min-width: 80em)": {
  250. "color": "#555"
  251. },
  252. }
  253. }
  254. )
  255. )
  256. },
  257. ),
  258. (
  259. {
  260. "_hover": {
  261. "color": ["#111", "#222", "#333", "#444", "#555"],
  262. "background_color": ["#FFF", "#EEE", "#DDD", "#CCC", "#BBB"],
  263. }
  264. },
  265. {},
  266. {
  267. "css": LiteralVar.create(
  268. Style(
  269. {
  270. "&:hover": {
  271. "@media screen and (min-width: 0)": {
  272. "color": "#111",
  273. "backgroundColor": "#FFF",
  274. },
  275. "@media screen and (min-width: 30em)": {
  276. "color": "#222",
  277. "backgroundColor": "#EEE",
  278. },
  279. "@media screen and (min-width: 48em)": {
  280. "color": "#333",
  281. "backgroundColor": "#DDD",
  282. },
  283. "@media screen and (min-width: 62em)": {
  284. "color": "#444",
  285. "backgroundColor": "#CCC",
  286. },
  287. "@media screen and (min-width: 80em)": {
  288. "color": "#555",
  289. "backgroundColor": "#BBB",
  290. },
  291. }
  292. }
  293. )
  294. )
  295. },
  296. ),
  297. (
  298. {
  299. "_hover": {
  300. "color": ["#111", "#222", "#333", "#444", "#555"],
  301. "background_color": "#FFF",
  302. }
  303. },
  304. {},
  305. {
  306. "css": LiteralVar.create(
  307. Style(
  308. {
  309. "&:hover": {
  310. "@media screen and (min-width: 0)": {"color": "#111"},
  311. "@media screen and (min-width: 30em)": {
  312. "color": "#222"
  313. },
  314. "@media screen and (min-width: 48em)": {
  315. "color": "#333"
  316. },
  317. "@media screen and (min-width: 62em)": {
  318. "color": "#444"
  319. },
  320. "@media screen and (min-width: 80em)": {
  321. "color": "#555"
  322. },
  323. "backgroundColor": "#FFF",
  324. }
  325. }
  326. )
  327. )
  328. },
  329. ),
  330. ],
  331. )
  332. def test_style_via_component(
  333. kwargs: dict[str, Any],
  334. style_dict: dict[str, Any],
  335. expected_get_style: dict[str, Any],
  336. ):
  337. """Pass kwargs and style_dict to a component and assert the final, combined style dict.
  338. Args:
  339. kwargs: The kwargs to pass to the component.
  340. style_dict: The style_dict to pass to the component.
  341. expected_get_style: The expected style dict.
  342. """
  343. comp = rx.el.div(style=style_dict, **kwargs) # pyright: ignore [reportArgumentType]
  344. compare_dict_of_var(comp._get_style(), expected_get_style)
  345. class StyleState(rx.State):
  346. """Style vars in a substate."""
  347. color: str = "hotpink"
  348. color2: str = "red"
  349. @pytest.mark.parametrize(
  350. ("kwargs", "expected_get_style"),
  351. [
  352. (
  353. {"color": StyleState.color},
  354. {"css": LiteralVar.create(Style({"color": StyleState.color}))},
  355. ),
  356. (
  357. {"color": f"dark{StyleState.color}"},
  358. {
  359. "css": Var(
  360. _js_expr=f'({{ ["color"] : ("dark"+{StyleState.color}) }})'
  361. ).to(Mapping[str, str])
  362. },
  363. ),
  364. (
  365. {"color": StyleState.color, "_hover": {"color": StyleState.color2}},
  366. {
  367. "css": LiteralVar.create(
  368. Style(
  369. {
  370. "color": StyleState.color,
  371. "&:hover": {"color": StyleState.color2},
  372. }
  373. )
  374. )
  375. },
  376. ),
  377. (
  378. {"color": [StyleState.color, "gray", StyleState.color2, "yellow", "blue"]},
  379. {
  380. "css": LiteralVar.create(
  381. Style(
  382. {
  383. "@media screen and (min-width: 0)": {
  384. "color": StyleState.color
  385. },
  386. "@media screen and (min-width: 30em)": {"color": "gray"},
  387. "@media screen and (min-width: 48em)": {
  388. "color": StyleState.color2
  389. },
  390. "@media screen and (min-width: 62em)": {"color": "yellow"},
  391. "@media screen and (min-width: 80em)": {"color": "blue"},
  392. }
  393. )
  394. )
  395. },
  396. ),
  397. (
  398. {
  399. "_hover": [
  400. {"color": StyleState.color},
  401. {"color": StyleState.color2},
  402. {"color": "#333"},
  403. {"color": "#444"},
  404. {"color": "#555"},
  405. ]
  406. },
  407. {
  408. "css": LiteralVar.create(
  409. Style(
  410. {
  411. "&:hover": {
  412. "@media screen and (min-width: 0)": {
  413. "color": StyleState.color
  414. },
  415. "@media screen and (min-width: 30em)": {
  416. "color": StyleState.color2
  417. },
  418. "@media screen and (min-width: 48em)": {
  419. "color": "#333"
  420. },
  421. "@media screen and (min-width: 62em)": {
  422. "color": "#444"
  423. },
  424. "@media screen and (min-width: 80em)": {
  425. "color": "#555"
  426. },
  427. }
  428. }
  429. )
  430. )
  431. },
  432. ),
  433. (
  434. {
  435. "_hover": {
  436. "color": [
  437. StyleState.color,
  438. StyleState.color2,
  439. "#333",
  440. "#444",
  441. "#555",
  442. ]
  443. }
  444. },
  445. {
  446. "css": LiteralVar.create(
  447. Style(
  448. {
  449. "&:hover": {
  450. "@media screen and (min-width: 0)": {
  451. "color": StyleState.color
  452. },
  453. "@media screen and (min-width: 30em)": {
  454. "color": StyleState.color2
  455. },
  456. "@media screen and (min-width: 48em)": {
  457. "color": "#333"
  458. },
  459. "@media screen and (min-width: 62em)": {
  460. "color": "#444"
  461. },
  462. "@media screen and (min-width: 80em)": {
  463. "color": "#555"
  464. },
  465. }
  466. }
  467. )
  468. )
  469. },
  470. ),
  471. ],
  472. )
  473. def test_style_via_component_with_state(
  474. kwargs: dict[str, Any],
  475. expected_get_style: dict[str, Any],
  476. ):
  477. """Pass kwargs to a component with state vars and assert the final, combined style dict.
  478. Args:
  479. kwargs: The kwargs to pass to the component.
  480. expected_get_style: The expected style dict.
  481. """
  482. comp = rx.el.div(**kwargs)
  483. assert (
  484. VarData.merge(comp.style._var_data)
  485. == expected_get_style["css"]._get_all_var_data()
  486. )
  487. # Assert that style values are equal.
  488. compare_dict_of_var(comp._get_style(), expected_get_style)
  489. def test_evaluate_style_namespaces():
  490. """Test that namespaces get converted to component create functions."""
  491. style_dict = {rx.text: {"color": "blue"}}
  492. assert rx.text.__call__ not in style_dict
  493. style_dict = evaluate_style_namespaces(style_dict) # pyright: ignore [reportArgumentType]
  494. assert rx.text.__call__ in style_dict
  495. def test_style_update_with_var_data():
  496. """Test that .update with a Style containing VarData works."""
  497. red_var = LiteralVar.create("red")._replace(
  498. merge_var_data=VarData(hooks={"const red = true": None}),
  499. )
  500. blue_var = LiteralVar.create("blue")._replace(
  501. merge_var_data=VarData(hooks={"const blue = true": None}),
  502. )
  503. s1 = Style(
  504. {
  505. "color": red_var,
  506. }
  507. )
  508. s2 = Style()
  509. s2.update(s1, background_color=f"{blue_var}ish")
  510. assert str(LiteralVar.create(s2)) == str(
  511. LiteralVar.create({"color": "red", "backgroundColor": "blueish"})
  512. )
  513. assert s2._var_data is not None
  514. assert "const red = true" in s2._var_data.hooks
  515. assert "const blue = true" in s2._var_data.hooks
  516. s3 = s1 | s2
  517. assert s3._var_data is not None
  518. assert "_varData" not in s3