Explorar el Código

reorganize all tests in a single top folder (#3981)

* lift node version restraint to allow more recent version if already installed

* add node test for latest version

* change python version

* use purple for debug logs

* update workflow

* add playwright dev dependency

* update workflow

* change test

* oops

* improve test

* update test

* fix tests

* mv units tests to a subfolder

* reorganize tests

* fix install

* update test_state

* revert node changes and only keep new tests organization

* move integration tests in tests/integration

* fix integration workflow

* fix dockerfile workflow

* fix dockerfile workflow 2

* fix shared_state
Thomas Brandého hace 7 meses
padre
commit
3f538865b5
Se han modificado 100 ficheros con 294 adiciones y 86 borrados
  1. 39 0
      .github/workflows/check_node_latest.yml
  2. 1 1
      .github/workflows/integration_app_harness.yml
  3. 2 2
      .github/workflows/reflex_init_in_docker_test.yml
  4. 3 3
      .github/workflows/unit_tests.yml
  5. 1 1
      .pre-commit-config.yaml
  6. 1 1
      CONTRIBUTING.md
  7. 164 75
      poetry.lock
  8. 2 0
      pyproject.toml
  9. 1 1
      reflex/utils/console.py
  10. 0 0
      tests/integration/__init__.py
  11. 0 0
      tests/integration/conftest.py
  12. 0 0
      tests/integration/init-test/Dockerfile
  13. 1 1
      tests/integration/init-test/in_docker_test_script.sh
  14. 0 0
      tests/integration/shared/state.py
  15. 0 0
      tests/integration/test_background_task.py
  16. 0 0
      tests/integration/test_call_script.py
  17. 0 0
      tests/integration/test_client_storage.py
  18. 0 0
      tests/integration/test_component_state.py
  19. 0 0
      tests/integration/test_computed_vars.py
  20. 0 0
      tests/integration/test_connection_banner.py
  21. 0 0
      tests/integration/test_deploy_url.py
  22. 0 0
      tests/integration/test_dynamic_components.py
  23. 0 0
      tests/integration/test_dynamic_routes.py
  24. 0 0
      tests/integration/test_event_actions.py
  25. 0 0
      tests/integration/test_event_chain.py
  26. 0 0
      tests/integration/test_exception_handlers.py
  27. 0 0
      tests/integration/test_form_submit.py
  28. 0 0
      tests/integration/test_input.py
  29. 0 0
      tests/integration/test_large_state.py
  30. 0 0
      tests/integration/test_lifespan.py
  31. 0 0
      tests/integration/test_login_flow.py
  32. 0 0
      tests/integration/test_media.py
  33. 0 0
      tests/integration/test_navigation.py
  34. 0 0
      tests/integration/test_server_side_event.py
  35. 1 1
      tests/integration/test_shared_state.py
  36. 0 0
      tests/integration/test_state_inheritance.py
  37. 0 0
      tests/integration/test_table.py
  38. 0 0
      tests/integration/test_tailwind.py
  39. 0 0
      tests/integration/test_upload.py
  40. 0 0
      tests/integration/test_urls.py
  41. 0 0
      tests/integration/test_var_operations.py
  42. 0 0
      tests/integration/utils.py
  43. 73 0
      tests/test_node_version.py
  44. 5 0
      tests/units/__init__.py
  45. 0 0
      tests/units/compiler/__init__.py
  46. 0 0
      tests/units/compiler/test_compiler.py
  47. 0 0
      tests/units/compiler/test_compiler_utils.py
  48. 0 0
      tests/units/components/__init__.py
  49. 0 0
      tests/units/components/base/test_bare.py
  50. 0 0
      tests/units/components/base/test_link.py
  51. 0 0
      tests/units/components/base/test_script.py
  52. 0 0
      tests/units/components/core/__init__.py
  53. 0 0
      tests/units/components/core/test_banner.py
  54. 0 0
      tests/units/components/core/test_colors.py
  55. 0 0
      tests/units/components/core/test_cond.py
  56. 0 0
      tests/units/components/core/test_debounce.py
  57. 0 0
      tests/units/components/core/test_foreach.py
  58. 0 0
      tests/units/components/core/test_html.py
  59. 0 0
      tests/units/components/core/test_match.py
  60. 0 0
      tests/units/components/core/test_responsive.py
  61. 0 0
      tests/units/components/core/test_upload.py
  62. 0 0
      tests/units/components/datadisplay/__init__.py
  63. 0 0
      tests/units/components/datadisplay/conftest.py
  64. 0 0
      tests/units/components/datadisplay/test_code.py
  65. 0 0
      tests/units/components/datadisplay/test_dataeditor.py
  66. 0 0
      tests/units/components/datadisplay/test_datatable.py
  67. 0 0
      tests/units/components/el/test_html.py
  68. 0 0
      tests/units/components/forms/__init__.py
  69. 0 0
      tests/units/components/forms/test_form.py
  70. 0 0
      tests/units/components/forms/test_uploads.py
  71. 0 0
      tests/units/components/graphing/__init__.py
  72. 0 0
      tests/units/components/graphing/test_plotly.py
  73. 0 0
      tests/units/components/graphing/test_recharts.py
  74. 0 0
      tests/units/components/layout/__init__.py
  75. 0 0
      tests/units/components/lucide/test_icon.py
  76. 0 0
      tests/units/components/media/__init__.py
  77. 0 0
      tests/units/components/media/test_image.py
  78. 0 0
      tests/units/components/radix/test_icon_button.py
  79. 0 0
      tests/units/components/radix/test_layout.py
  80. 0 0
      tests/units/components/recharts/test_cartesian.py
  81. 0 0
      tests/units/components/recharts/test_polar.py
  82. 0 0
      tests/units/components/test_component.py
  83. 0 0
      tests/units/components/test_component_future_annotations.py
  84. 0 0
      tests/units/components/test_component_state.py
  85. 0 0
      tests/units/components/test_tag.py
  86. 0 0
      tests/units/components/typography/__init__.py
  87. 0 0
      tests/units/components/typography/test_markdown.py
  88. 0 0
      tests/units/conftest.py
  89. 0 0
      tests/units/experimental/custom_script.js
  90. 0 0
      tests/units/experimental/test_assets.py
  91. 0 0
      tests/units/middleware/__init__.py
  92. 0 0
      tests/units/middleware/conftest.py
  93. 0 0
      tests/units/middleware/test_hydrate_middleware.py
  94. 0 0
      tests/units/states/__init__.py
  95. 0 0
      tests/units/states/mutation.py
  96. 0 0
      tests/units/states/upload.py
  97. 0 0
      tests/units/test_app.py
  98. 0 0
      tests/units/test_attribute_access_type.py
  99. 0 0
      tests/units/test_base.py
  100. 0 0
      tests/units/test_config.py

+ 39 - 0
.github/workflows/check_node_latest.yml

@@ -0,0 +1,39 @@
+name: integration-node-latest
+
+on:
+    push:
+        branches:
+            - main
+    pull_request:
+        branches:
+            - main
+
+env:
+    TELEMETRY_ENABLED: false
+
+jobs:
+    check_latest_node:
+        runs-on: ubuntu-latest
+        strategy:
+            matrix:
+                python-version: ['3.12']
+                node-version: ['node']
+        steps:
+            - uses: actions/checkout@v4
+            - uses: ./.github/actions/setup_build_env
+              with:
+                python-version: ${{ matrix.python-version }}
+                run-poetry-install: true
+                create-venv-at-path: .venv
+            - uses: actions/setup-node@v4
+              with:
+                node-version: ${{ matrix.node-version }}
+            - run: |
+                poetry run uv pip install pyvirtualdisplay pillow
+                poetry run playwright install --with-deps
+            - run: |
+                # poetry run pytest tests/test_node_version.py
+                poetry run pytest tests/integration
+  
+
+

+ 1 - 1
.github/workflows/integration_app_harness.yml

@@ -51,7 +51,7 @@ jobs:
           SCREENSHOT_DIR: /tmp/screenshots
           REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
         run: |
-          poetry run pytest integration
+          poetry run pytest tests/integration
       - uses: actions/upload-artifact@v4
         name: Upload failed test screenshots
         if: always()

+ 2 - 2
.github/workflows/reflex_init_in_docker_test.yml

@@ -28,5 +28,5 @@ jobs:
           # Run reflex init in a docker container
 
           # cwd is repo root
-          docker build -f integration/init-test/Dockerfile -t reflex-init-test integration/init-test
-          docker run --rm -v $(pwd):/reflex-repo/ reflex-init-test /reflex-repo/integration/init-test/in_docker_test_script.sh
+          docker build -f tests/integration/init-test/Dockerfile -t reflex-init-test tests/integration/init-test
+          docker run --rm -v $(pwd):/reflex-repo/ reflex-init-test /reflex-repo/tests/integration/init-test/in_docker_test_script.sh

+ 3 - 3
.github/workflows/unit_tests.yml

@@ -65,17 +65,17 @@ jobs:
       - name: Run unit tests
         run: |
           export PYTHONUNBUFFERED=1
-          poetry run pytest tests --cov --no-cov-on-fail --cov-report=
+          poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=
       - name: Run unit tests w/ redis
         if: ${{ matrix.os == 'ubuntu-latest' }}
         run: |
           export PYTHONUNBUFFERED=1
           export REDIS_URL=redis://localhost:6379
-          poetry run pytest tests --cov --no-cov-on-fail --cov-report=
+          poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=
       # Change to explicitly install v1 when reflex-hosting-cli is compatible with v2
       - name: Run unit tests w/ pydantic v1
         run: |
           export PYTHONUNBUFFERED=1
           poetry run uv pip install "pydantic~=1.10"
-          poetry run pytest tests --cov --no-cov-on-fail --cov-report=
+          poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=
       - run: poetry run coverage html

+ 1 - 1
.pre-commit-config.yaml

@@ -6,7 +6,7 @@ repos:
     rev: v0.4.10
     hooks:
       - id: ruff-format
-        args: [integration, reflex, tests]
+        args: [reflex, tests]
       - id: ruff
         args: ["--fix", "--exit-non-zero-on-fix"]
         exclude: '^integration/benchmarks/'

+ 1 - 1
CONTRIBUTING.md

@@ -69,7 +69,7 @@ In your `reflex` directory run make sure all the unit tests are still passing us
 This will fail if code coverage is below 70%.
 
 ``` bash
-poetry run pytest tests --cov --no-cov-on-fail --cov-report= 
+poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= 
 ```
 
 Next make sure all the following tests pass. This ensures that every new change has proper documentation and type checking.

+ 164 - 75
poetry.lock

@@ -616,84 +616,69 @@ typing = ["typing-extensions (>=4.12.2)"]
 
 [[package]]
 name = "greenlet"
-version = "3.1.1"
+version = "3.0.3"
 description = "Lightweight in-process concurrent programming"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
-    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
-    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"},
-    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"},
-    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"},
-    {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"},
-    {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"},
-    {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"},
-    {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"},
-    {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
-    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
-    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
-    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
-    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
-    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
-    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
-    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
-    {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
-    {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
-    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
-    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
-    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
-    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
-    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
-    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
-    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
-    {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
-    {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
-    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
-    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
-    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
-    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
-    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
-    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
-    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
-    {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
-    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
-    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
-    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
-    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
-    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
-    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
-    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
-    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"},
-    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"},
-    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"},
-    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"},
-    {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"},
-    {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"},
-    {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"},
-    {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"},
-    {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"},
-    {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"},
-    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"},
-    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"},
-    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"},
-    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"},
-    {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"},
-    {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"},
-    {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"},
-    {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"},
-    {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"},
-    {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"},
-    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"},
-    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"},
-    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"},
-    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"},
-    {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"},
-    {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"},
-    {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"},
-    {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"},
-    {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
-    {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
+    {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"},
+    {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"},
+    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"},
+    {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"},
+    {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"},
+    {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
+    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
+    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
+    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
+    {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
+    {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
+    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
+    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
+    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
+    {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
+    {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"},
+    {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"},
+    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"},
+    {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"},
+    {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"},
+    {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"},
+    {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"},
+    {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"},
+    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"},
+    {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"},
+    {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"},
+    {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"},
+    {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"},
+    {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"},
+    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"},
+    {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"},
+    {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"},
+    {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"},
+    {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
 ]
 
 [package.extras]
@@ -1527,6 +1512,26 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a
 test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
 type = ["mypy (>=1.11.2)"]
 
+[[package]]
+name = "playwright"
+version = "1.47.0"
+description = "A high-level API to automate web browsers"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "playwright-1.47.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f205df24edb925db1a4ab62f1ab0da06f14bb69e382efecfb0deedc4c7f4b8cd"},
+    {file = "playwright-1.47.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fc820faf6885f69a52ba4ec94124e575d3c4a4003bf29200029b4a4f2b2d0ab"},
+    {file = "playwright-1.47.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:8e212dc472ff19c7d46ed7e900191c7a786ce697556ac3f1615986ec3aa00341"},
+    {file = "playwright-1.47.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:a1935672531963e4b2a321de5aa59b982fb92463ee6e1032dd7326378e462955"},
+    {file = "playwright-1.47.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0a1b61473d6f7f39c5d77d4800b3cbefecb03344c90b98f3fbcae63294ad249"},
+    {file = "playwright-1.47.0-py3-none-win32.whl", hash = "sha256:1b977ed81f6bba5582617684a21adab9bad5676d90a357ebf892db7bdf4a9974"},
+    {file = "playwright-1.47.0-py3-none-win_amd64.whl", hash = "sha256:0ec1056042d2e86088795a503347407570bffa32cbe20748e5d4c93dba085280"},
+]
+
+[package.dependencies]
+greenlet = "3.0.3"
+pyee = "12.0.0"
+
 [[package]]
 name = "plotly"
 version = "5.24.1"
@@ -1750,6 +1755,23 @@ files = [
 [package.dependencies]
 typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
 
+[[package]]
+name = "pyee"
+version = "12.0.0"
+description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"},
+    {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"},
+]
+
+[package.dependencies]
+typing-extensions = "*"
+
+[package.extras]
+dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
+
 [[package]]
 name = "pygments"
 version = "2.18.0"
@@ -1845,6 +1867,24 @@ pytest = ">=7.0.0"
 docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
 testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
 
+[[package]]
+name = "pytest-base-url"
+version = "2.1.0"
+description = "pytest plugin for URL based testing"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6"},
+    {file = "pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+requests = ">=2.9"
+
+[package.extras]
+test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest-localserver (>=0.7.1)", "tox (>=3.24.5)"]
+
 [[package]]
 name = "pytest-benchmark"
 version = "4.0.0"
@@ -1900,6 +1940,23 @@ pytest = ">=6.2.5"
 [package.extras]
 dev = ["pre-commit", "pytest-asyncio", "tox"]
 
+[[package]]
+name = "pytest-playwright"
+version = "0.5.2"
+description = "A pytest wrapper with fixtures for Playwright to automate web browsers"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest_playwright-0.5.2-py3-none-any.whl", hash = "sha256:2c5720591364a1cdf66610b972ff8492512bc380953e043c85f705b78b2ed582"},
+    {file = "pytest_playwright-0.5.2.tar.gz", hash = "sha256:c6d603df9e6c50b35f057b0528e11d41c0963283e98c257267117f5ed6ba1924"},
+]
+
+[package.dependencies]
+playwright = ">=1.18"
+pytest = ">=6.2.4,<9.0.0"
+pytest-base-url = ">=1.0.0,<3.0.0"
+python-slugify = ">=6.0.0,<9.0.0"
+
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -1944,6 +2001,23 @@ files = [
     {file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"},
 ]
 
+[[package]]
+name = "python-slugify"
+version = "8.0.4"
+description = "A Python slugify application that also handles Unicode"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"},
+    {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"},
+]
+
+[package.dependencies]
+text-unidecode = ">=1.3"
+
+[package.extras]
+unidecode = ["Unidecode (>=1.1.1)"]
+
 [[package]]
 name = "python-socketio"
 version = "5.11.4"
@@ -2334,7 +2408,9 @@ python-versions = ">=3.7"
 files = [
     {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"},
     {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"},
+    {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"},
     {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"},
+    {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"},
     {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"},
     {file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"},
     {file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"},
@@ -2371,7 +2447,9 @@ files = [
     {file = "SQLAlchemy-2.0.35-cp38-cp38-win_amd64.whl", hash = "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee"},
     {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"},
     {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"},
+    {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"},
     {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"},
+    {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"},
     {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"},
     {file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"},
     {file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"},
@@ -2493,6 +2571,17 @@ files = [
 doc = ["reno", "sphinx"]
 test = ["pytest", "tornado (>=4.5)", "typeguard"]
 
+[[package]]
+name = "text-unidecode"
+version = "1.3"
+description = "The most basic Text::Unidecode port"
+optional = false
+python-versions = "*"
+files = [
+    {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
+    {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
+]
+
 [[package]]
 name = "toml"
 version = "0.10.2"
@@ -2922,4 +3011,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "df66545bb3f79e356e3a91866306f8b18b800aeadf1f98ac184644f986a83d95"
+content-hash = "adccd071775567aeefe219261aeb9e222906c865745f03edb1e770edc79c44ac"

+ 2 - 0
pyproject.toml

@@ -77,6 +77,8 @@ asynctest = ">=0.13.0,<1.0"
 pre-commit = ">=3.2.1"
 selenium = ">=4.11.0,<5.0"
 pytest-benchmark = ">=4.0.0,<5.0"
+playwright = ">=1.46.0"
+pytest-playwright = ">=0.5.1"
 
 [tool.poetry.scripts]
 reflex = "reflex.reflex:cli"

+ 1 - 1
reflex/utils/console.py

@@ -58,7 +58,7 @@ def debug(msg: str, **kwargs):
         kwargs: Keyword arguments to pass to the print function.
     """
     if is_debug():
-        msg_ = f"[blue]Debug: {msg}[/blue]"
+        msg_ = f"[purple]Debug: {msg}[/purple]"
         if progress := kwargs.pop("progress", None):
             progress.console.print(msg_, **kwargs)
         else:

+ 0 - 0
integration/__init__.py → tests/integration/__init__.py


+ 0 - 0
integration/conftest.py → tests/integration/conftest.py


+ 0 - 0
integration/init-test/Dockerfile → tests/integration/init-test/Dockerfile


+ 1 - 1
integration/init-test/in_docker_test_script.sh → tests/integration/init-test/in_docker_test_script.sh

@@ -13,7 +13,7 @@ function do_export () {
     reflex init --template "$template"
     reflex export
     (
-        cd "$SCRIPTPATH/../.."
+        cd "$SCRIPTPATH/../../.."
         scripts/integration.sh ~/"$template" dev
         pkill -9 -f 'next-server|python3' || true
         sleep 10

+ 0 - 0
integration/shared/state.py → tests/integration/shared/state.py


+ 0 - 0
integration/test_background_task.py → tests/integration/test_background_task.py


+ 0 - 0
integration/test_call_script.py → tests/integration/test_call_script.py


+ 0 - 0
integration/test_client_storage.py → tests/integration/test_client_storage.py


+ 0 - 0
integration/test_component_state.py → tests/integration/test_component_state.py


+ 0 - 0
integration/test_computed_vars.py → tests/integration/test_computed_vars.py


+ 0 - 0
integration/test_connection_banner.py → tests/integration/test_connection_banner.py


+ 0 - 0
integration/test_deploy_url.py → tests/integration/test_deploy_url.py


+ 0 - 0
integration/test_dynamic_components.py → tests/integration/test_dynamic_components.py


+ 0 - 0
integration/test_dynamic_routes.py → tests/integration/test_dynamic_routes.py


+ 0 - 0
integration/test_event_actions.py → tests/integration/test_event_actions.py


+ 0 - 0
integration/test_event_chain.py → tests/integration/test_event_chain.py


+ 0 - 0
integration/test_exception_handlers.py → tests/integration/test_exception_handlers.py


+ 0 - 0
integration/test_form_submit.py → tests/integration/test_form_submit.py


+ 0 - 0
integration/test_input.py → tests/integration/test_input.py


+ 0 - 0
integration/test_large_state.py → tests/integration/test_large_state.py


+ 0 - 0
integration/test_lifespan.py → tests/integration/test_lifespan.py


+ 0 - 0
integration/test_login_flow.py → tests/integration/test_login_flow.py


+ 0 - 0
integration/test_media.py → tests/integration/test_media.py


+ 0 - 0
integration/test_navigation.py → tests/integration/test_navigation.py


+ 0 - 0
integration/test_server_side_event.py → tests/integration/test_server_side_event.py


+ 1 - 1
integration/test_shared_state.py → tests/integration/test_shared_state.py

@@ -12,7 +12,7 @@ from reflex.testing import AppHarness, WebDriver
 def SharedStateApp():
     """Test that shared state works as expected."""
     import reflex as rx
-    from integration.shared.state import SharedState
+    from tests.integration.shared.state import SharedState
 
     class State(SharedState):
         pass

+ 0 - 0
integration/test_state_inheritance.py → tests/integration/test_state_inheritance.py


+ 0 - 0
integration/test_table.py → tests/integration/test_table.py


+ 0 - 0
integration/test_tailwind.py → tests/integration/test_tailwind.py


+ 0 - 0
integration/test_upload.py → tests/integration/test_upload.py


+ 0 - 0
integration/test_urls.py → tests/integration/test_urls.py


+ 0 - 0
integration/test_var_operations.py → tests/integration/test_var_operations.py


+ 0 - 0
integration/utils.py → tests/integration/utils.py


+ 73 - 0
tests/test_node_version.py

@@ -0,0 +1,73 @@
+"""Test for latest node version being able to run reflex."""
+
+from __future__ import annotations
+
+from typing import Any, Generator
+
+import httpx
+import pytest
+from playwright.sync_api import Page, expect
+
+from reflex.testing import AppHarness
+
+
+def TestNodeVersionApp():
+    """A test app for node latest version."""
+    import reflex as rx
+    from reflex.utils.prerequisites import get_node_version
+
+    class TestNodeVersionConfig(rx.Config):
+        pass
+
+    class TestNodeVersionState(rx.State):
+        @rx.var
+        def node_version(self) -> str:
+            return str(get_node_version())
+
+    app = rx.App()
+
+    @app.add_page
+    def index():
+        return rx.heading("Node Version check v", TestNodeVersionState.node_version)
+
+
+@pytest.fixture()
+def node_version_app(tmp_path) -> Generator[AppHarness, Any, None]:
+    """Fixture to start TestNodeVersionApp app at tmp_path via AppHarness.
+
+    Args:
+        tmp_path: pytest tmp_path fixture
+
+    Yields:
+        running AppHarness instance
+    """
+    with AppHarness.create(
+        root=tmp_path,
+        app_source=TestNodeVersionApp,  # type: ignore
+    ) as harness:
+        yield harness
+
+
+def test_node_version(node_version_app: AppHarness, page: Page):
+    """Test for latest node version being able to run reflex.
+
+    Args:
+        node_version_app: running AppHarness instance
+        page: playwright page instance
+    """
+
+    def get_latest_node_version():
+        response = httpx.get("https://nodejs.org/dist/index.json")
+        versions = response.json()
+
+        # Assuming the first entry in the API response is the most recent version
+        if versions:
+            latest_version = versions[0]["version"]
+            return latest_version
+        return None
+
+    assert node_version_app.frontend_url is not None
+    page.goto(node_version_app.frontend_url)
+    expect(page.get_by_role("heading")).to_have_text(
+        f"Node Version check {get_latest_node_version()}"
+    )

+ 5 - 0
tests/units/__init__.py

@@ -0,0 +1,5 @@
+"""Root directory for tests."""
+
+import os
+
+from reflex import constants

+ 0 - 0
tests/compiler/__init__.py → tests/units/compiler/__init__.py


+ 0 - 0
tests/compiler/test_compiler.py → tests/units/compiler/test_compiler.py


+ 0 - 0
tests/compiler/test_compiler_utils.py → tests/units/compiler/test_compiler_utils.py


+ 0 - 0
tests/components/__init__.py → tests/units/components/__init__.py


+ 0 - 0
tests/components/base/test_bare.py → tests/units/components/base/test_bare.py


+ 0 - 0
tests/components/base/test_link.py → tests/units/components/base/test_link.py


+ 0 - 0
tests/components/base/test_script.py → tests/units/components/base/test_script.py


+ 0 - 0
tests/components/core/__init__.py → tests/units/components/core/__init__.py


+ 0 - 0
tests/components/core/test_banner.py → tests/units/components/core/test_banner.py


+ 0 - 0
tests/components/core/test_colors.py → tests/units/components/core/test_colors.py


+ 0 - 0
tests/components/core/test_cond.py → tests/units/components/core/test_cond.py


+ 0 - 0
tests/components/core/test_debounce.py → tests/units/components/core/test_debounce.py


+ 0 - 0
tests/components/core/test_foreach.py → tests/units/components/core/test_foreach.py


+ 0 - 0
tests/components/core/test_html.py → tests/units/components/core/test_html.py


+ 0 - 0
tests/components/core/test_match.py → tests/units/components/core/test_match.py


+ 0 - 0
tests/components/core/test_responsive.py → tests/units/components/core/test_responsive.py


+ 0 - 0
tests/components/core/test_upload.py → tests/units/components/core/test_upload.py


+ 0 - 0
tests/components/datadisplay/__init__.py → tests/units/components/datadisplay/__init__.py


+ 0 - 0
tests/components/datadisplay/conftest.py → tests/units/components/datadisplay/conftest.py


+ 0 - 0
tests/components/datadisplay/test_code.py → tests/units/components/datadisplay/test_code.py


+ 0 - 0
tests/components/datadisplay/test_dataeditor.py → tests/units/components/datadisplay/test_dataeditor.py


+ 0 - 0
tests/components/datadisplay/test_datatable.py → tests/units/components/datadisplay/test_datatable.py


+ 0 - 0
tests/components/el/test_html.py → tests/units/components/el/test_html.py


+ 0 - 0
tests/components/forms/__init__.py → tests/units/components/forms/__init__.py


+ 0 - 0
tests/components/forms/test_form.py → tests/units/components/forms/test_form.py


+ 0 - 0
tests/components/forms/test_uploads.py → tests/units/components/forms/test_uploads.py


+ 0 - 0
tests/components/graphing/__init__.py → tests/units/components/graphing/__init__.py


+ 0 - 0
tests/components/graphing/test_plotly.py → tests/units/components/graphing/test_plotly.py


+ 0 - 0
tests/components/graphing/test_recharts.py → tests/units/components/graphing/test_recharts.py


+ 0 - 0
tests/components/layout/__init__.py → tests/units/components/layout/__init__.py


+ 0 - 0
tests/components/lucide/test_icon.py → tests/units/components/lucide/test_icon.py


+ 0 - 0
tests/components/media/__init__.py → tests/units/components/media/__init__.py


+ 0 - 0
tests/components/media/test_image.py → tests/units/components/media/test_image.py


+ 0 - 0
tests/components/radix/test_icon_button.py → tests/units/components/radix/test_icon_button.py


+ 0 - 0
tests/components/radix/test_layout.py → tests/units/components/radix/test_layout.py


+ 0 - 0
tests/components/recharts/test_cartesian.py → tests/units/components/recharts/test_cartesian.py


+ 0 - 0
tests/components/recharts/test_polar.py → tests/units/components/recharts/test_polar.py


+ 0 - 0
tests/components/test_component.py → tests/units/components/test_component.py


+ 0 - 0
tests/components/test_component_future_annotations.py → tests/units/components/test_component_future_annotations.py


+ 0 - 0
tests/components/test_component_state.py → tests/units/components/test_component_state.py


+ 0 - 0
tests/components/test_tag.py → tests/units/components/test_tag.py


+ 0 - 0
tests/components/typography/__init__.py → tests/units/components/typography/__init__.py


+ 0 - 0
tests/components/typography/test_markdown.py → tests/units/components/typography/test_markdown.py


+ 0 - 0
tests/conftest.py → tests/units/conftest.py


+ 0 - 0
tests/experimental/custom_script.js → tests/units/experimental/custom_script.js


+ 0 - 0
tests/experimental/test_assets.py → tests/units/experimental/test_assets.py


+ 0 - 0
tests/middleware/__init__.py → tests/units/middleware/__init__.py


+ 0 - 0
tests/middleware/conftest.py → tests/units/middleware/conftest.py


+ 0 - 0
tests/middleware/test_hydrate_middleware.py → tests/units/middleware/test_hydrate_middleware.py


+ 0 - 0
tests/states/__init__.py → tests/units/states/__init__.py


+ 0 - 0
tests/states/mutation.py → tests/units/states/mutation.py


+ 0 - 0
tests/states/upload.py → tests/units/states/upload.py


+ 0 - 0
tests/test_app.py → tests/units/test_app.py


+ 0 - 0
tests/test_attribute_access_type.py → tests/units/test_attribute_access_type.py


+ 0 - 0
tests/test_base.py → tests/units/test_base.py


+ 0 - 0
tests/test_config.py → tests/units/test_config.py


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio