test_compiler.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import importlib.util
  2. import os
  3. from pathlib import Path
  4. import pytest
  5. from pytest_mock import MockerFixture
  6. from reflex import constants
  7. from reflex.compiler import compiler, utils
  8. from reflex.constants.compiler import PageNames
  9. from reflex.utils.imports import ImportVar, ParsedImportDict
  10. @pytest.mark.parametrize(
  11. "fields,test_default,test_rest",
  12. [
  13. (
  14. [ImportVar(tag="axios", is_default=True)],
  15. "axios",
  16. [],
  17. ),
  18. (
  19. [ImportVar(tag="foo"), ImportVar(tag="bar")],
  20. "",
  21. ["bar", "foo"],
  22. ),
  23. (
  24. [
  25. ImportVar(tag="axios", is_default=True),
  26. ImportVar(tag="foo"),
  27. ImportVar(tag="bar"),
  28. ],
  29. "axios",
  30. ["bar", "foo"],
  31. ),
  32. ],
  33. )
  34. def test_compile_import_statement(
  35. fields: list[ImportVar], test_default: str, test_rest: str
  36. ):
  37. """Test the compile_import_statement function.
  38. Args:
  39. fields: The fields to import.
  40. test_default: The expected output of default library.
  41. test_rest: The expected output rest libraries.
  42. """
  43. default, rest = utils.compile_import_statement(fields)
  44. assert default == test_default
  45. assert sorted(rest) == test_rest
  46. @pytest.mark.parametrize(
  47. "import_dict,test_dicts",
  48. [
  49. ({}, []),
  50. (
  51. {"axios": [ImportVar(tag="axios", is_default=True)]},
  52. [{"lib": "axios", "default": "axios", "rest": []}],
  53. ),
  54. (
  55. {"axios": [ImportVar(tag="foo"), ImportVar(tag="bar")]},
  56. [{"lib": "axios", "default": "", "rest": ["bar", "foo"]}],
  57. ),
  58. (
  59. {
  60. "axios": [
  61. ImportVar(tag="axios", is_default=True),
  62. ImportVar(tag="foo"),
  63. ImportVar(tag="bar"),
  64. ],
  65. "react": [ImportVar(tag="react", is_default=True)],
  66. },
  67. [
  68. {"lib": "axios", "default": "axios", "rest": ["bar", "foo"]},
  69. {"lib": "react", "default": "react", "rest": []},
  70. ],
  71. ),
  72. (
  73. {"": [ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")]},
  74. [
  75. {"lib": "lib1.js", "default": "", "rest": []},
  76. {"lib": "lib2.js", "default": "", "rest": []},
  77. ],
  78. ),
  79. (
  80. {
  81. "": [ImportVar(tag="lib1.js"), ImportVar(tag="lib2.js")],
  82. "axios": [ImportVar(tag="axios", is_default=True)],
  83. },
  84. [
  85. {"lib": "lib1.js", "default": "", "rest": []},
  86. {"lib": "lib2.js", "default": "", "rest": []},
  87. {"lib": "axios", "default": "axios", "rest": []},
  88. ],
  89. ),
  90. ],
  91. )
  92. def test_compile_imports(import_dict: ParsedImportDict, test_dicts: list[dict]):
  93. """Test the compile_imports function.
  94. Args:
  95. import_dict: The import dictionary.
  96. test_dicts: The expected output.
  97. """
  98. imports = utils.compile_imports(import_dict)
  99. for import_dict, test_dict in zip(imports, test_dicts, strict=True):
  100. assert import_dict["lib"] == test_dict["lib"]
  101. assert import_dict["default"] == test_dict["default"]
  102. assert (
  103. sorted(
  104. import_dict["rest"],
  105. key=lambda i: i if isinstance(i, str) else (i.tag or ""),
  106. )
  107. == test_dict["rest"]
  108. )
  109. def test_compile_stylesheets(tmp_path: Path, mocker: MockerFixture):
  110. """Test that stylesheets compile correctly.
  111. Args:
  112. tmp_path: The test directory.
  113. mocker: Pytest mocker object.
  114. """
  115. project = tmp_path / "test_project"
  116. project.mkdir()
  117. assets_dir = project / "assets"
  118. assets_dir.mkdir()
  119. (assets_dir / "style.css").write_text(
  120. "button.rt-Button {\n\tborder-radius:unset !important;\n}"
  121. )
  122. mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
  123. mocker.patch(
  124. "reflex.compiler.compiler.get_web_dir",
  125. return_value=project / constants.Dirs.WEB,
  126. )
  127. mocker.patch(
  128. "reflex.compiler.utils.get_web_dir", return_value=project / constants.Dirs.WEB
  129. )
  130. stylesheets = [
  131. "https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple",
  132. "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css",
  133. "/style.css",
  134. "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css",
  135. ]
  136. assert compiler.compile_root_stylesheet(stylesheets) == (
  137. str(
  138. project
  139. / constants.Dirs.WEB
  140. / "styles"
  141. / (PageNames.STYLESHEET_ROOT + ".css")
  142. ),
  143. "@import url('./tailwind.css'); \n"
  144. "@import url('https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple'); \n"
  145. "@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css'); \n"
  146. "@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css'); \n"
  147. "@import url('./style.css'); \n",
  148. )
  149. assert (project / constants.Dirs.WEB / "styles" / "style.css").read_text() == (
  150. assets_dir / "style.css"
  151. ).read_text()
  152. def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker: MockerFixture):
  153. if importlib.util.find_spec("sass") is None:
  154. pytest.skip(
  155. 'The `libsass` package is required to compile sass/scss stylesheet files. Run `pip install "libsass>=0.23.0"`.'
  156. )
  157. if os.name == "nt":
  158. pytest.skip("Skipping test on Windows")
  159. project = tmp_path / "test_project"
  160. project.mkdir()
  161. assets_dir = project / "assets"
  162. assets_dir.mkdir()
  163. assets_preprocess_dir = assets_dir / "preprocess"
  164. assets_preprocess_dir.mkdir()
  165. (assets_dir / "style.css").write_text(
  166. "button.rt-Button {\n\tborder-radius:unset !important;\n}"
  167. )
  168. (assets_preprocess_dir / "styles_a.sass").write_text(
  169. "button.rt-Button\n\tborder-radius:unset !important"
  170. )
  171. (assets_preprocess_dir / "styles_b.scss").write_text(
  172. "button.rt-Button {\n\tborder-radius:unset !important;\n}"
  173. )
  174. mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
  175. mocker.patch(
  176. "reflex.compiler.compiler.get_web_dir",
  177. return_value=project / constants.Dirs.WEB,
  178. )
  179. mocker.patch(
  180. "reflex.compiler.utils.get_web_dir", return_value=project / constants.Dirs.WEB
  181. )
  182. stylesheets = [
  183. "/style.css",
  184. "/preprocess/styles_a.sass",
  185. "/preprocess/styles_b.scss",
  186. ]
  187. assert compiler.compile_root_stylesheet(stylesheets) == (
  188. str(
  189. project
  190. / constants.Dirs.WEB
  191. / "styles"
  192. / (PageNames.STYLESHEET_ROOT + ".css")
  193. ),
  194. "@import url('./tailwind.css'); \n"
  195. "@import url('./style.css'); \n"
  196. f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}'); \n"
  197. f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}'); \n",
  198. )
  199. stylesheets = [
  200. "/style.css",
  201. "/preprocess", # this is a folder containing "styles_a.sass" and "styles_b.scss"
  202. ]
  203. assert compiler.compile_root_stylesheet(stylesheets) == (
  204. str(
  205. project
  206. / constants.Dirs.WEB
  207. / "styles"
  208. / (PageNames.STYLESHEET_ROOT + ".css")
  209. ),
  210. "@import url('./tailwind.css'); \n"
  211. "@import url('./style.css'); \n"
  212. f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}'); \n"
  213. f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}'); \n",
  214. )
  215. assert (project / constants.Dirs.WEB / "styles" / "style.css").read_text() == (
  216. assets_dir / "style.css"
  217. ).read_text()
  218. expected_result = "button.rt-Button{border-radius:unset !important}\n"
  219. assert (
  220. project / constants.Dirs.WEB / "styles" / "preprocess" / "styles_a.css"
  221. ).read_text() == expected_result
  222. assert (
  223. project / constants.Dirs.WEB / "styles" / "preprocess" / "styles_b.css"
  224. ).read_text() == expected_result
  225. def test_compile_stylesheets_exclude_tailwind(tmp_path, mocker: MockerFixture):
  226. """Test that Tailwind is excluded if tailwind config is explicitly set to None.
  227. Args:
  228. tmp_path: The test directory.
  229. mocker: Pytest mocker object.
  230. """
  231. project = tmp_path / "test_project"
  232. project.mkdir()
  233. assets_dir = project / "assets"
  234. assets_dir.mkdir()
  235. mock = mocker.Mock()
  236. mocker.patch.object(mock, "tailwind", None)
  237. mocker.patch("reflex.compiler.compiler.get_config", return_value=mock)
  238. (assets_dir / "style.css").touch()
  239. mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
  240. stylesheets = [
  241. "/style.css",
  242. ]
  243. assert compiler.compile_root_stylesheet(stylesheets) == (
  244. str(Path(".web") / "styles" / (PageNames.STYLESHEET_ROOT + ".css")),
  245. "@import url('./style.css'); \n",
  246. )
  247. def test_compile_nonexistent_stylesheet(tmp_path, mocker: MockerFixture):
  248. """Test that an error is thrown for non-existent stylesheets.
  249. Args:
  250. tmp_path: The test directory.
  251. mocker: Pytest mocker object.
  252. """
  253. project = tmp_path / "test_project"
  254. project.mkdir()
  255. assets_dir = project / "assets"
  256. assets_dir.mkdir()
  257. mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
  258. stylesheets = ["/style.css"]
  259. with pytest.raises(FileNotFoundError):
  260. compiler.compile_root_stylesheet(stylesheets)
  261. def test_create_document_root():
  262. """Test that the document root is created correctly."""
  263. # Test with no components.
  264. root = utils.create_document_root()
  265. root.render()
  266. assert isinstance(root, utils.Html)
  267. assert isinstance(root.children[0], utils.DocumentHead)
  268. # Default language.
  269. assert root.lang == "en" # pyright: ignore [reportAttributeAccessIssue]
  270. # No children in head.
  271. assert len(root.children[0].children) == 0
  272. # Test with components.
  273. comps = [
  274. utils.NextScript.create(src="foo.js"),
  275. utils.NextScript.create(src="bar.js"),
  276. ]
  277. root = utils.create_document_root(
  278. head_components=comps,
  279. html_lang="rx",
  280. html_custom_attrs={"project": "reflex"},
  281. )
  282. # Two children in head.
  283. assert isinstance(root, utils.Html)
  284. assert len(root.children[0].children) == 2
  285. assert root.lang == "rx" # pyright: ignore [reportAttributeAccessIssue]
  286. assert isinstance(root.custom_attrs, dict)
  287. assert root.custom_attrs == {"project": "reflex"}