test_compiler.py 9.3 KB

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