test_compiler.py 10 KB

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