From f3884de3bf4380939966d3840d79a51903865ad9 Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Sun, 14 May 2023 14:58:59 +0200 Subject: [PATCH 01/12] setup tests, linting and codestyle checks --- .gitignore | 4 ++++ pyproject.toml | 29 +++++++++++++++++++++++++++-- tox.ini | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index c18dd8d..3659bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ __pycache__/ +.tox +.vscode +.coverage +*.egg-info diff --git a/pyproject.toml b/pyproject.toml index a8f43fe..0614b47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,27 @@ -[tool.black] -line-length = 79 +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "tests.settings" +django_find_project = false +pythonpath = "." +addopts = ''' + --cov=django_vite + --cov-report html + --cov-report term-missing + --cov-branch +''' + +[tool.ruff] +select = [ + "E", # pycodestyle + "F", # pyflakes + "C", # pyupgrade + "B", # bugbear, + "PT", # pytest, + "SIM", # simplify, + "DJ", # django +] + +# do not autofix the following violations due to bad DX: +unfixable = [ + "F401", # Module imported but unused + "F841", # Unused variables +] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5076ea8 --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +envlist = + codestyle, + lint, + {py38,py39}-django{32,40,41,42}, + {py310,py311}-django{41,42,-latest}, +isolated_build = true +minversion = 1.9 + +[testenv] +description = run unit tests +deps = + pytest>=7 + pytest-cov + pytest-django + pytest-sugar + django32: Django>=3.2,<4.0 + django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 + django-latest: https://github.com/django/django/archive/main.tar.gz +commands = + pytest {posargs:tests} +ignore_outcome = + django-latest: True + +[testenv:codestyle] +basepython = python3 +commands = + black --check --diff . +deps = + black +skip_install = true + +[testenv:lint] +commands = + ruff check django_vite +deps = + ruff +skip_install = true From 40561b92747a205c4151643c77c757b0eb228c4d Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Sun, 14 May 2023 14:59:15 +0200 Subject: [PATCH 02/12] add a simple smoke test with some standard django settings --- tests/settings.py | 28 ++++++++++++++++++++++++++++ tests/test_singleton.py | 8 ++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/settings.py create mode 100644 tests/test_singleton.py diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..189a195 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,28 @@ +import os + +BASE_DIR = os.path.dirname(__file__) + +STATIC_URL = "static" +DJANGO_VITE_DEV_MODE = True +DJANGO_VITE_ASSETS_PATH = "/" +USE_TZ = True + + +INSTALLED_APPS = [ + "django_vite", +] + +TEMPLATE_DEBUG = True +TEMPLATE_DIRS = (os.path.join(BASE_DIR, "templates"),) + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": TEMPLATE_DIRS, + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [], + "debug": TEMPLATE_DEBUG, + }, + }, +] diff --git a/tests/test_singleton.py b/tests/test_singleton.py new file mode 100644 index 0000000..7ee8935 --- /dev/null +++ b/tests/test_singleton.py @@ -0,0 +1,8 @@ +import pytest + +from django_vite.templatetags.django_vite import DjangoViteAssetLoader + + +def test_django_vite_asset_loader_cannot_be_instantiated(): + with pytest.raises(RuntimeError): + DjangoViteAssetLoader() From 16fe3fbe649c5dccaf8d69f7991b8256af20e130 Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Sun, 14 May 2023 15:05:07 +0200 Subject: [PATCH 03/12] add Github Action to run Tox test matrix --- .github/workflows/test.yml | 31 +++++++++++++++++++++++++++++++ tox.ini | 7 +++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ff9e985 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Run Test Matrix + +on: + push: + branches: [master] + pull_request: + branches: [master] + +concurrency: + group: ${{ github.head_ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/tox.ini b/tox.ini index 5076ea8..c746865 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,13 @@ envlist = isolated_build = true minversion = 1.9 +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + [testenv] description = run unit tests deps = From 8625b951172771e5486e18efbd8d5b16014e20d9 Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Fri, 2 Jun 2023 10:56:58 +0200 Subject: [PATCH 04/12] fix codestyle warning --- django_vite/apps.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/django_vite/apps.py b/django_vite/apps.py index ab781bc..98dae96 100644 --- a/django_vite/apps.py +++ b/django_vite/apps.py @@ -1,3 +1,5 @@ +from contextlib import suppress + from django.apps import AppConfig from django.core.checks import Warning, register @@ -9,12 +11,10 @@ class DjangoViteAppConfig(AppConfig): verbose_name = "Django Vite" def ready(self) -> None: - try: - # Create Loader instance at startup to prevent threading problems. + with suppress(RuntimeError): + # Create Loader instance at startup to prevent threading problems, + # but do not crash while doing so. DjangoViteAssetLoader.instance() - except RuntimeError: - # Just continue, the system check below outputs a warning. - pass @register From 8ae432d63849ad912185502539ab7967a2ba0998 Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Fri, 2 Jun 2023 11:08:57 +0200 Subject: [PATCH 05/12] update pre-commit config --- .github/workflows/pre-commit.yml | 2 +- .pre-commit-config.yaml | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index e373bbc..0bcc3ed 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -12,4 +12,4 @@ jobs: - uses: "actions/setup-python@v4" with: python-version: 3.x - - uses: "pre-commit/action@v2.0.3" + - uses: "pre-commit/action@v3.0.0" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5d7f53..c1391dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: "https://github.com/pre-commit/pre-commit-hooks" - rev: "v4.1.0" + rev: "v4.4.0" hooks: - id: "check-toml" - id: "check-yaml" @@ -10,15 +10,10 @@ repos: - id: "trailing-whitespace" - repo: "https://github.com/psf/black" - rev: "22.3.0" + rev: "23.3.0" hooks: - id: "black" - - repo: "https://github.com/PyCQA/flake8" - rev: "4.0.1" - hooks: - - id: "flake8" - - repo: "https://github.com/commitizen-tools/commitizen" rev: master hooks: From 05223cc7ac80aa1c65617460db11f0acbe97aabb Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Fri, 2 Jun 2023 11:10:36 +0200 Subject: [PATCH 06/12] re-format code with black --- django_vite/templatetags/django_vite.py | 26 +++++++------------------ 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/django_vite/templatetags/django_vite.py b/django_vite/templatetags/django_vite.py index 0941cbc..26432e6 100644 --- a/django_vite/templatetags/django_vite.py +++ b/django_vite/templatetags/django_vite.py @@ -25,9 +25,7 @@ ) # Default Vite server port. -DJANGO_VITE_DEV_SERVER_PORT = getattr( - settings, "DJANGO_VITE_DEV_SERVER_PORT", 3000 -) +DJANGO_VITE_DEV_SERVER_PORT = getattr(settings, "DJANGO_VITE_DEV_SERVER_PORT", 3000) # Default Vite server path to HMR script. DJANGO_VITE_WS_CLIENT_URL = getattr( @@ -42,12 +40,10 @@ # Must be included in your "STATICFILES_DIRS". # In Django production mode this folder need to be collected as static # files using "python manage.py collectstatic". -DJANGO_VITE_ASSETS_PATH = Path(getattr(settings, "DJANGO_VITE_ASSETS_PATH")) +DJANGO_VITE_ASSETS_PATH = Path(settings.DJANGO_VITE_ASSETS_PATH) # Prefix for STATIC_URL -DJANGO_VITE_STATIC_URL_PREFIX = getattr( - settings, "DJANGO_VITE_STATIC_URL_PREFIX", "" -) +DJANGO_VITE_STATIC_URL_PREFIX = getattr(settings, "DJANGO_VITE_STATIC_URL_PREFIX", "") DJANGO_VITE_STATIC_ROOT = ( DJANGO_VITE_ASSETS_PATH @@ -68,9 +64,7 @@ settings, "DJANGO_VITE_LEGACY_POLYFILLS_MOTIF", "legacy-polyfills" ) -DJANGO_VITE_STATIC_URL = urljoin( - settings.STATIC_URL, DJANGO_VITE_STATIC_URL_PREFIX -) +DJANGO_VITE_STATIC_URL = urljoin(settings.STATIC_URL, DJANGO_VITE_STATIC_URL_PREFIX) # Make sure 'DJANGO_VITE_STATIC_URL' finish with a '/' if DJANGO_VITE_STATIC_URL[-1] != "/": @@ -496,9 +490,7 @@ def _generate_script_tag(src: str, attrs: Dict[str, str]) -> str: str -- The script tag. """ - attrs_str = " ".join( - [f'{key}="{value}"' for key, value in attrs.items()] - ) + attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()]) return f'' @@ -731,9 +723,7 @@ def vite_legacy_polyfills(**kwargs: Dict[str, str]) -> str: str -- The script tag to the polyfills. """ - return DjangoViteAssetLoader.instance().generate_vite_legacy_polyfills( - **kwargs - ) + return DjangoViteAssetLoader.instance().generate_vite_legacy_polyfills(**kwargs) @register.simple_tag @@ -765,9 +755,7 @@ def vite_legacy_asset( assert path is not None - return DjangoViteAssetLoader.instance().generate_vite_legacy_asset( - path, **kwargs - ) + return DjangoViteAssetLoader.instance().generate_vite_legacy_asset(path, **kwargs) @register.simple_tag From 57d07a357d79603448decf3d9e37fc469671a54a Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Fri, 2 Jun 2023 11:23:03 +0200 Subject: [PATCH 07/12] add troves to clarify for which django and python versions Django-Vite is intended --- setup.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index bbfd4b1..f2e4533 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="django-vite", version="2.1.3", - description="Integration of ViteJS in a Django project.", + description="Integration of Vite in a Django project.", long_description=README, long_description_content_type="text/markdown", author="MrBin99", @@ -20,13 +20,23 @@ include_package_data=True, packages=find_packages(), requires=[ - "Django (>=1.11)", + "Django (>=3.2)", ], install_requires=[ - "Django>=1.11", + "Django>=3.2", ], classifiers=[ "License :: OSI Approved :: Apache Software License", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], - extras_require={"dev": ["black", "flake8"]}, + extras_require={"dev": ["black"]}, ) From 30bf4a4b9cad66438c9a51cbd2266b44be71ad83 Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Mon, 5 Jun 2023 21:07:52 +0200 Subject: [PATCH 08/12] fix linting errors --- django_vite/templatetags/django_vite.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/django_vite/templatetags/django_vite.py b/django_vite/templatetags/django_vite.py index 26432e6..b5b54f4 100644 --- a/django_vite/templatetags/django_vite.py +++ b/django_vite/templatetags/django_vite.py @@ -420,15 +420,14 @@ def _parse_manifest(self) -> None: """ try: - manifest_file = open(DJANGO_VITE_MANIFEST_PATH, "r") - manifest_content = manifest_file.read() - manifest_file.close() + with open(DJANGO_VITE_MANIFEST_PATH, "r") as manifest_file: + manifest_content = manifest_file.read() self._manifest = json.loads(manifest_content) except Exception as error: raise RuntimeError( f"Cannot read Vite manifest file at " f"{DJANGO_VITE_MANIFEST_PATH} : {str(error)}" - ) + ) from error @classmethod def instance(cls): From b9c94ade87eec24e8c44ffce05b7462287cbf403 Mon Sep 17 00:00:00 2001 From: Thijs Kramer Date: Mon, 5 Jun 2023 22:24:38 +0200 Subject: [PATCH 09/12] add tests for templatetags --- ...test_singleton.py => test_asset_loader.py} | 0 tests/test_templatetags.py | 150 ++++++++++++++++++ tox.ini | 1 + 3 files changed, 151 insertions(+) rename tests/{test_singleton.py => test_asset_loader.py} (100%) create mode 100644 tests/test_templatetags.py diff --git a/tests/test_singleton.py b/tests/test_asset_loader.py similarity index 100% rename from tests/test_singleton.py rename to tests/test_asset_loader.py diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py new file mode 100644 index 0000000..79bb5c3 --- /dev/null +++ b/tests/test_templatetags.py @@ -0,0 +1,150 @@ +import pytest +from bs4 import BeautifulSoup +from django.template import Context, Template, TemplateSyntaxError + + +@pytest.fixture() +def override_setting(monkeypatch): + def _override_setting(setting, value): + monkeypatch.setattr( + f"django_vite.templatetags.django_vite.{setting}", + value, + ) + + return _override_setting + + +def test_vite_hmr_client_returns_script_tag(): + template = Template( + """ + {% load django_vite %} + {% vite_hmr_client %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "http://localhost:3000/static/@vite/client" + assert script_tag["type"] == "module" + + +def test_vite_hmr_client_kwargs(): + template = Template( + """ + {% load django_vite %} + {% vite_hmr_client blocking="render" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag.has_attr("blocking") + assert script_tag["blocking"] == "render" + + +def test_vite_hmr_client_returns_nothing_with_dev_mode_off(settings, monkeypatch): + settings.DJANGO_VITE_DEV_MODE = False + monkeypatch.setattr( + "django_vite.templatetags.django_vite.DJANGO_VITE_DEV_MODE", + settings.DJANGO_VITE_DEV_MODE, + ) + template = Template( + """ + {% load django_vite %} + {% vite_hmr_client %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + assert str(soup).strip() == "" + + +def test_vite_hmr_client_uses_correct_settings(override_setting): + override_setting("DJANGO_VITE_DEV_SERVER_PROTOCOL", "https") + override_setting("DJANGO_VITE_DEV_SERVER_HOST", "127.0.0.2") + override_setting("DJANGO_VITE_DEV_SERVER_PORT", "5174") + override_setting("DJANGO_VITE_STATIC_URL", "static/") + override_setting("DJANGO_VITE_WS_CLIENT_URL", "foo/bar") + + template = Template( + """ + {% load django_vite %} + {% vite_hmr_client %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "https://127.0.0.2:5174/static/foo/bar" + + +def test_vite_asset_returns_script_tags(): + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/entry.tsx" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "http://localhost:3000/static/src/entry.tsx" + assert script_tag["type"] == "module" + + +def test_vite_asset_raises_without_path(): + with pytest.raises(TemplateSyntaxError): + Template( + """ + {% load django_vite %} + {% vite_asset %} + """ + ) + + +def test_vite_react_refresh_happy_flow(): + template = Template( + """ + {% load django_vite %} + {% vite_react_refresh %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.script + assert not script_tag.has_attr("src") + assert script_tag.has_attr("type") + assert script_tag["type"] == "module" + assert "__vite_plugin_react_preamble_installed__" in script_tag.text + assert "http://localhost:3000/static/@react-refresh" in script_tag.text + + +def test_vite_react_refresh_returns_nothing_with_dev_mode_off(settings, monkeypatch): + settings.DJANGO_VITE_DEV_MODE = False + monkeypatch.setattr( + "django_vite.templatetags.django_vite.DJANGO_VITE_DEV_MODE", + settings.DJANGO_VITE_DEV_MODE, + ) + template = Template( + """ + {% load django_vite %} + {% vite_react_refresh %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + assert str(soup).strip() == "" + + +def test_vite_react_refresh_url_setting(override_setting): + override_setting("DJANGO_VITE_REACT_REFRESH_URL", "foobar") + template = Template( + """ + {% load django_vite %} + {% vite_react_refresh %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.script + assert "http://localhost:3000/static/foobar" in script_tag.text diff --git a/tox.ini b/tox.ini index c746865..193fa9c 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ python = [testenv] description = run unit tests deps = + beautifulsoup4 pytest>=7 pytest-cov pytest-django From 5cf2c0659b70cfd36dd4bdd21db0fcc750e9b634 Mon Sep 17 00:00:00 2001 From: Nick Ivons <14187277+Niicck@users.noreply.github.com> Date: Mon, 16 Oct 2023 00:27:33 -0700 Subject: [PATCH 10/12] Add tests to achieve 100% Test Coverage (#91) * create patch_settings fixture * 100% test coverage --------- Co-authored-by: Ben Beecher <120373+gone@users.noreply.github.com> Co-authored-by: Eddy Brown Co-authored-by: Thijs Kramer <1274867+thijskramer@users.noreply.github.com> Co-authored-by: Niicck --- django_vite/apps.py | 5 +- django_vite/exceptions.py | 10 ++ django_vite/templatetags/django_vite.py | 53 +++---- pyproject.toml | 4 + tests/conftest.py | 46 ++++++ .../custom-motif-polyfills-manifest.json | 24 +++ .../staticfiles/custom/prefix/manifest.json | 72 +++++++++ tests/data/staticfiles/manifest.json | 72 +++++++++ .../data/staticfiles/polyfills-manifest.json | 24 +++ tests/settings.py | 13 +- tests/test_asset_loader.py | 8 - tests/test_templatetags.py | 150 ------------------ tests/tests/templatetags/test_vite_asset.py | 124 +++++++++++++++ .../tests/templatetags/test_vite_asset_url.py | 43 +++++ .../templatetags/test_vite_hmr_client.py | 67 ++++++++ .../templatetags/test_vite_legacy_asset.py | 49 ++++++ .../test_vite_legacy_polyfills.py | 78 +++++++++ .../templatetags/test_vite_preload_asset.py | 45 ++++++ .../templatetags/test_vite_react_refresh.py | 74 +++++++++ tests/tests/test_asset_loader.py | 33 ++++ tox.ini | 21 +++ 21 files changed, 819 insertions(+), 196 deletions(-) create mode 100644 django_vite/exceptions.py create mode 100644 tests/conftest.py create mode 100644 tests/data/staticfiles/custom-motif-polyfills-manifest.json create mode 100644 tests/data/staticfiles/custom/prefix/manifest.json create mode 100644 tests/data/staticfiles/manifest.json create mode 100644 tests/data/staticfiles/polyfills-manifest.json delete mode 100644 tests/test_asset_loader.py delete mode 100644 tests/test_templatetags.py create mode 100644 tests/tests/templatetags/test_vite_asset.py create mode 100644 tests/tests/templatetags/test_vite_asset_url.py create mode 100644 tests/tests/templatetags/test_vite_hmr_client.py create mode 100644 tests/tests/templatetags/test_vite_legacy_asset.py create mode 100644 tests/tests/templatetags/test_vite_legacy_polyfills.py create mode 100644 tests/tests/templatetags/test_vite_preload_asset.py create mode 100644 tests/tests/templatetags/test_vite_react_refresh.py create mode 100644 tests/tests/test_asset_loader.py diff --git a/django_vite/apps.py b/django_vite/apps.py index 98dae96..4bc1bdc 100644 --- a/django_vite/apps.py +++ b/django_vite/apps.py @@ -3,6 +3,7 @@ from django.apps import AppConfig from django.core.checks import Warning, register +from .exceptions import DjangoViteManifestError from .templatetags.django_vite import DjangoViteAssetLoader @@ -11,7 +12,7 @@ class DjangoViteAppConfig(AppConfig): verbose_name = "Django Vite" def ready(self) -> None: - with suppress(RuntimeError): + with suppress(DjangoViteManifestError): # Create Loader instance at startup to prevent threading problems, # but do not crash while doing so. DjangoViteAssetLoader.instance() @@ -25,7 +26,7 @@ def check_loader_instance(**kwargs): # Make Loader instance at startup to prevent threading problems DjangoViteAssetLoader.instance() return [] - except RuntimeError as exception: + except DjangoViteManifestError as exception: return [ Warning( exception, diff --git a/django_vite/exceptions.py b/django_vite/exceptions.py new file mode 100644 index 0000000..1030ca8 --- /dev/null +++ b/django_vite/exceptions.py @@ -0,0 +1,10 @@ +class DjangoViteManifestError(RuntimeError): + """Manifest parsing failed.""" + + pass + + +class DjangoViteAssetNotFoundError(RuntimeError): + """Vite Asset could not be found.""" + + pass diff --git a/django_vite/templatetags/django_vite.py b/django_vite/templatetags/django_vite.py index b5b54f4..fdb3304 100644 --- a/django_vite/templatetags/django_vite.py +++ b/django_vite/templatetags/django_vite.py @@ -1,6 +1,6 @@ import json from pathlib import Path -from typing import Dict, List, Callable +from typing import Callable, Dict, List from urllib.parse import urljoin from django import template @@ -8,8 +8,9 @@ from django.conf import settings from django.utils.safestring import mark_safe -register = template.Library() +from django_vite.exceptions import DjangoViteAssetNotFoundError, DjangoViteManifestError +register = template.Library() # If using in development or production mode. DJANGO_VITE_DEV_MODE = getattr(settings, "DJANGO_VITE_DEV_MODE", False) @@ -104,7 +105,7 @@ def generate_vite_asset( script tags. Raises: - RuntimeError: If cannot find the file path in the + DjangoViteAssetNotFoundError: If cannot find the file path in the manifest (only in production). Returns: @@ -119,7 +120,7 @@ def generate_vite_asset( ) if not self._manifest or path not in self._manifest: - raise RuntimeError( + raise DjangoViteAssetNotFoundError( f"Cannot find {path} in Vite manifest " f"at {DJANGO_VITE_MANIFEST_PATH}" ) @@ -152,9 +153,7 @@ def generate_vite_asset( for dep in manifest_entry.get("imports", []): dep_manifest_entry = self._manifest[dep] dep_file = dep_manifest_entry["file"] - url = DjangoViteAssetLoader._generate_production_server_url( - dep_file - ) + url = DjangoViteAssetLoader._generate_production_server_url(dep_file) tags.append( DjangoViteAssetLoader._generate_preload_tag( url, @@ -182,7 +181,7 @@ def preload_vite_asset( str -- All tags to preload this file in your HTML page. Raises: - RuntimeError: If cannot find the file path in the + DjangoViteAssetNotFoundError: if cannot find the file path in the manifest. Returns: @@ -193,7 +192,7 @@ def preload_vite_asset( return "" if not self._manifest or path not in self._manifest: - raise RuntimeError( + raise DjangoViteAssetNotFoundError( f"Cannot find {path} in Vite manifest " f"at {DJANGO_VITE_MANIFEST_PATH}" ) @@ -210,9 +209,7 @@ def preload_vite_asset( } manifest_file = manifest_entry["file"] - url = DjangoViteAssetLoader._generate_production_server_url( - manifest_file - ) + url = DjangoViteAssetLoader._generate_production_server_url(manifest_file) tags.append( DjangoViteAssetLoader._generate_preload_tag( url, @@ -227,9 +224,7 @@ def preload_vite_asset( for dep in manifest_entry.get("imports", []): dep_manifest_entry = self._manifest[dep] dep_file = dep_manifest_entry["file"] - url = DjangoViteAssetLoader._generate_production_server_url( - dep_file - ) + url = DjangoViteAssetLoader._generate_production_server_url(dep_file) tags.append( DjangoViteAssetLoader._generate_preload_tag( url, @@ -285,10 +280,8 @@ def _generate_css_files_of_asset( if "css" in manifest_entry: for css_path in manifest_entry["css"]: if css_path not in already_processed: - url = ( - DjangoViteAssetLoader._generate_production_server_url( - css_path - ) + url = DjangoViteAssetLoader._generate_production_server_url( + css_path ) tags.append(tag_generator(url)) @@ -305,7 +298,7 @@ def generate_vite_asset_url(self, path: str) -> str: path {str} -- Path to a Vite asset. Raises: - RuntimeError: If cannot find the asset path in the + DjangoViteAssetNotFoundError: If cannot find the asset path in the manifest (only in production). Returns: @@ -316,7 +309,7 @@ def generate_vite_asset_url(self, path: str) -> str: return DjangoViteAssetLoader._generate_vite_server_url(path) if not self._manifest or path not in self._manifest: - raise RuntimeError( + raise DjangoViteAssetNotFoundError( f"Cannot find {path} in Vite manifest " f"at {DJANGO_VITE_MANIFEST_PATH}" ) @@ -340,7 +333,7 @@ def generate_vite_legacy_polyfills( script tags. Raises: - RuntimeError: If polyfills path not found inside + DjangoViteAssetNotFoundError: If polyfills path not found inside the 'manifest.json' (only in production). Returns: @@ -361,7 +354,7 @@ def generate_vite_legacy_polyfills( attrs=scripts_attrs, ) - raise RuntimeError( + raise DjangoViteAssetNotFoundError( f"Vite legacy polyfills not found in manifest " f"at {DJANGO_VITE_MANIFEST_PATH}" ) @@ -385,7 +378,7 @@ def generate_vite_legacy_asset( script tags. Raises: - RuntimeError: If cannot find the asset path in the + DjangoViteAssetNotFoundError: If cannot find the asset path in the manifest (only in production). Returns: @@ -396,7 +389,7 @@ def generate_vite_legacy_asset( return "" if not self._manifest or path not in self._manifest: - raise RuntimeError( + raise DjangoViteAssetNotFoundError( f"Cannot find {path} in Vite manifest " f"at {DJANGO_VITE_MANIFEST_PATH}" ) @@ -416,7 +409,8 @@ def _parse_manifest(self) -> None: Read and parse the Vite manifest file. Raises: - RuntimeError: if cannot load the file or JSON in file is malformed. + DjangoViteManifestError: if cannot load the file or JSON in file is + malformed. """ try: @@ -424,7 +418,7 @@ def _parse_manifest(self) -> None: manifest_content = manifest_file.read() self._manifest = json.loads(manifest_content) except Exception as error: - raise RuntimeError( + raise DjangoViteManifestError( f"Cannot read Vite manifest file at " f"{DJANGO_VITE_MANIFEST_PATH} : {str(error)}" ) from error @@ -523,9 +517,7 @@ def _generate_stylesheet_preload_tag(href: str) -> str: @staticmethod def _generate_preload_tag(href: str, attrs: Dict[str, str]) -> str: - attrs_str = " ".join( - [f'{key}="{value}"' for key, value in attrs.items()] - ) + attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()]) return f'' @@ -673,7 +665,6 @@ def vite_preload_asset( manifest (only in production). """ - assert path is not None return DjangoViteAssetLoader.instance().preload_vite_asset(path) diff --git a/pyproject.toml b/pyproject.toml index 0614b47..c332ae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,12 @@ addopts = ''' --cov-report html --cov-report term-missing --cov-branch + --cov-fail-under=100 ''' +[tool.black] +line-length = 88 + [tool.ruff] select = [ "E", # pycodestyle diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2eedec1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,46 @@ +from typing import Dict, Any +import pytest +from importlib import reload +from django.apps import apps +from django_vite.templatetags import django_vite + + +def reload_django_vite(): + reload(django_vite) + django_vite_app_config = apps.get_app_config("django_vite") + django_vite_app_config.ready() + + +@pytest.fixture() +def patch_settings(settings): + """ + 1. Patch new settings into django.conf.settings. + 2. Reload django_vite module so that variables on the module level that use settings + get recalculated. + 3. Restore the original settings once the test is over. + + TODO: refactor django_vite so that we don't set variables on the module level using + settings. + """ + __PYTEST_EMPTY__ = "__PYTEST_EMPTY__" + original_settings_cache = {} + + def _patch_settings(new_settings: Dict[str, Any]): + for key, value in new_settings.items(): + original_settings_cache[key] = getattr(settings, key, __PYTEST_EMPTY__) + setattr(settings, key, value) + reload_django_vite() + + yield _patch_settings + + for key, value in original_settings_cache.items(): + if value == __PYTEST_EMPTY__: + delattr(settings, key) + else: + setattr(settings, key, value) + reload_django_vite() + + +@pytest.fixture() +def dev_mode_off(patch_settings): + patch_settings({"DJANGO_VITE_DEV_MODE": False}) diff --git a/tests/data/staticfiles/custom-motif-polyfills-manifest.json b/tests/data/staticfiles/custom-motif-polyfills-manifest.json new file mode 100644 index 0000000..c954ade --- /dev/null +++ b/tests/data/staticfiles/custom-motif-polyfills-manifest.json @@ -0,0 +1,24 @@ +{ + "../../vite/custom-motif-legacy": { + "file": "assets/polyfills-legacy-6e7a4b9c.js", + "isEntry": true, + "src": "../../vite/custom-motif-legacy" + }, + "src/entry-legacy.ts": { + "file": "assets/entry-legacy-4c50596f.js", + "isEntry": true, + "src": "src/entry-legacy.ts" + }, + "src/entry.css": { + "file": "assets/entry-5e7d9c21.css", + "src": "src/entry.css" + }, + "src/entry.ts": { + "css": [ + "assets/entry-5e7d9c21.css" + ], + "file": "assets/entry-8a2f6b3d.js", + "isEntry": true, + "src": "src/entry.ts" + } +} diff --git a/tests/data/staticfiles/custom/prefix/manifest.json b/tests/data/staticfiles/custom/prefix/manifest.json new file mode 100644 index 0000000..fbe4a74 --- /dev/null +++ b/tests/data/staticfiles/custom/prefix/manifest.json @@ -0,0 +1,72 @@ +{ + "src/entry.ts": { + "css": ["assets/entry-74d0d5dd.css"], + "file": "assets/entry-29e38a60.js", + "imports": [ + "_vue.esm-bundler-96356fb1.js", + "_index-62f37ad0.js", + "_vue.esm-bundler-49e6b475.js", + "__plugin-vue_export-helper-c27b6911.js", + "_messages-a0a9e13b.js", + "_use-outside-click-224980bf.js", + "_use-resolve-button-type-c5656cba.js", + "_use-event-listener-153dc639.js", + "_hidden-84ddb9a5.js", + "_apiClient-01d20438.js", + "_pinia-5d7892fd.js" + ], + "isEntry": true, + "src": "entry.ts" + }, + "src/entry.css": { + "file": "assets/entry-74d0d5dd.css", + "src": "entry.css" + }, + "src/extra.css": { + "file": "assets/extra-a9f3b2c1.css", + "src": "extra.css" + }, + "_vue.esm-bundler-96356fb1.js": { + "file": "vue.esm-bundler-96356fb1.js" + }, + "_index-62f37ad0.js": { + "file": "index-62f37ad0.js", + "imports": ["_vue.esm-bundler-49e6b475.js"] + }, + "_vue.esm-bundler-49e6b475.js": { + "file": "vue.esm-bundler-49e6b475.js", + "imports": ["_vue.esm-bundler-96356fb1.js"] + }, + "__plugin-vue_export-helper-c27b6911.js": { + "file": "_plugin-vue_export-helper-c27b6911.js" + }, + "_messages-a0a9e13b.js": { + "css": ["assets/extra-a9f3b2c1.css"], + "file": "messages-a0a9e13b.js", + "imports": ["_pinia-5d7892fd.js", "_vue.esm-bundler-96356fb1.js"] + }, + "_pinia-5d7892fd.js": { + "file": "pinia-5d7892fd.js", + "imports": ["_vue.esm-bundler-96356fb1.js"] + }, + "_use-outside-click-224980bf.js": { + "file": "use-outside-click-224980bf.js", + "imports": ["_vue.esm-bundler-96356fb1.js"] + }, + "_use-resolve-button-type-c5656cba.js": { + "file": "use-resolve-button-type-c5656cba.js", + "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"] + }, + "_use-event-listener-153dc639.js": { + "file": "use-event-listener-153dc639.js", + "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"] + }, + "_hidden-84ddb9a5.js": { + "file": "hidden-84ddb9a5.js", + "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"] + }, + "_apiClient-01d20438.js": { + "css": ["assets/extra-a9f3b2c1.css"], + "file": "apiClient-01d20438.js" + } +} diff --git a/tests/data/staticfiles/manifest.json b/tests/data/staticfiles/manifest.json new file mode 100644 index 0000000..fbe4a74 --- /dev/null +++ b/tests/data/staticfiles/manifest.json @@ -0,0 +1,72 @@ +{ + "src/entry.ts": { + "css": ["assets/entry-74d0d5dd.css"], + "file": "assets/entry-29e38a60.js", + "imports": [ + "_vue.esm-bundler-96356fb1.js", + "_index-62f37ad0.js", + "_vue.esm-bundler-49e6b475.js", + "__plugin-vue_export-helper-c27b6911.js", + "_messages-a0a9e13b.js", + "_use-outside-click-224980bf.js", + "_use-resolve-button-type-c5656cba.js", + "_use-event-listener-153dc639.js", + "_hidden-84ddb9a5.js", + "_apiClient-01d20438.js", + "_pinia-5d7892fd.js" + ], + "isEntry": true, + "src": "entry.ts" + }, + "src/entry.css": { + "file": "assets/entry-74d0d5dd.css", + "src": "entry.css" + }, + "src/extra.css": { + "file": "assets/extra-a9f3b2c1.css", + "src": "extra.css" + }, + "_vue.esm-bundler-96356fb1.js": { + "file": "vue.esm-bundler-96356fb1.js" + }, + "_index-62f37ad0.js": { + "file": "index-62f37ad0.js", + "imports": ["_vue.esm-bundler-49e6b475.js"] + }, + "_vue.esm-bundler-49e6b475.js": { + "file": "vue.esm-bundler-49e6b475.js", + "imports": ["_vue.esm-bundler-96356fb1.js"] + }, + "__plugin-vue_export-helper-c27b6911.js": { + "file": "_plugin-vue_export-helper-c27b6911.js" + }, + "_messages-a0a9e13b.js": { + "css": ["assets/extra-a9f3b2c1.css"], + "file": "messages-a0a9e13b.js", + "imports": ["_pinia-5d7892fd.js", "_vue.esm-bundler-96356fb1.js"] + }, + "_pinia-5d7892fd.js": { + "file": "pinia-5d7892fd.js", + "imports": ["_vue.esm-bundler-96356fb1.js"] + }, + "_use-outside-click-224980bf.js": { + "file": "use-outside-click-224980bf.js", + "imports": ["_vue.esm-bundler-96356fb1.js"] + }, + "_use-resolve-button-type-c5656cba.js": { + "file": "use-resolve-button-type-c5656cba.js", + "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"] + }, + "_use-event-listener-153dc639.js": { + "file": "use-event-listener-153dc639.js", + "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"] + }, + "_hidden-84ddb9a5.js": { + "file": "hidden-84ddb9a5.js", + "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"] + }, + "_apiClient-01d20438.js": { + "css": ["assets/extra-a9f3b2c1.css"], + "file": "apiClient-01d20438.js" + } +} diff --git a/tests/data/staticfiles/polyfills-manifest.json b/tests/data/staticfiles/polyfills-manifest.json new file mode 100644 index 0000000..ddebd6a --- /dev/null +++ b/tests/data/staticfiles/polyfills-manifest.json @@ -0,0 +1,24 @@ +{ + "../../vite/legacy-polyfills-legacy": { + "file": "assets/polyfills-legacy-f4c2b91e.js", + "isEntry": true, + "src": "../../vite/legacy-polyfills-legacy" + }, + "src/entry-legacy.ts": { + "file": "assets/entry-legacy-32083566.js", + "isEntry": true, + "src": "src/entry-legacy.ts" + }, + "src/entry.css": { + "file": "assets/entry-74d0d5dd.css", + "src": "src/entry.css" + }, + "src/entry.ts": { + "css": [ + "assets/entry-74d0d5dd.css" + ], + "file": "assets/entry-2e8a3a7a.js", + "isEntry": true, + "src": "src/entry.ts" + } +} diff --git a/tests/settings.py b/tests/settings.py index 189a195..541df85 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,13 +1,11 @@ import os +from pathlib import Path -BASE_DIR = os.path.dirname(__file__) +BASE_DIR = Path(__file__).resolve().parent -STATIC_URL = "static" -DJANGO_VITE_DEV_MODE = True -DJANGO_VITE_ASSETS_PATH = "/" +STATIC_URL = "/static/" USE_TZ = True - INSTALLED_APPS = [ "django_vite", ] @@ -26,3 +24,8 @@ }, }, ] + +STATIC_ROOT = BASE_DIR / "data" / "staticfiles" + +DJANGO_VITE_DEV_MODE = True +DJANGO_VITE_ASSETS_PATH = "/" diff --git a/tests/test_asset_loader.py b/tests/test_asset_loader.py deleted file mode 100644 index 7ee8935..0000000 --- a/tests/test_asset_loader.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -from django_vite.templatetags.django_vite import DjangoViteAssetLoader - - -def test_django_vite_asset_loader_cannot_be_instantiated(): - with pytest.raises(RuntimeError): - DjangoViteAssetLoader() diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py deleted file mode 100644 index 79bb5c3..0000000 --- a/tests/test_templatetags.py +++ /dev/null @@ -1,150 +0,0 @@ -import pytest -from bs4 import BeautifulSoup -from django.template import Context, Template, TemplateSyntaxError - - -@pytest.fixture() -def override_setting(monkeypatch): - def _override_setting(setting, value): - monkeypatch.setattr( - f"django_vite.templatetags.django_vite.{setting}", - value, - ) - - return _override_setting - - -def test_vite_hmr_client_returns_script_tag(): - template = Template( - """ - {% load django_vite %} - {% vite_hmr_client %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - script_tag = soup.find("script") - assert script_tag["src"] == "http://localhost:3000/static/@vite/client" - assert script_tag["type"] == "module" - - -def test_vite_hmr_client_kwargs(): - template = Template( - """ - {% load django_vite %} - {% vite_hmr_client blocking="render" %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - script_tag = soup.find("script") - assert script_tag.has_attr("blocking") - assert script_tag["blocking"] == "render" - - -def test_vite_hmr_client_returns_nothing_with_dev_mode_off(settings, monkeypatch): - settings.DJANGO_VITE_DEV_MODE = False - monkeypatch.setattr( - "django_vite.templatetags.django_vite.DJANGO_VITE_DEV_MODE", - settings.DJANGO_VITE_DEV_MODE, - ) - template = Template( - """ - {% load django_vite %} - {% vite_hmr_client %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - assert str(soup).strip() == "" - - -def test_vite_hmr_client_uses_correct_settings(override_setting): - override_setting("DJANGO_VITE_DEV_SERVER_PROTOCOL", "https") - override_setting("DJANGO_VITE_DEV_SERVER_HOST", "127.0.0.2") - override_setting("DJANGO_VITE_DEV_SERVER_PORT", "5174") - override_setting("DJANGO_VITE_STATIC_URL", "static/") - override_setting("DJANGO_VITE_WS_CLIENT_URL", "foo/bar") - - template = Template( - """ - {% load django_vite %} - {% vite_hmr_client %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - script_tag = soup.find("script") - assert script_tag["src"] == "https://127.0.0.2:5174/static/foo/bar" - - -def test_vite_asset_returns_script_tags(): - template = Template( - """ - {% load django_vite %} - {% vite_asset "src/entry.tsx" %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - script_tag = soup.find("script") - assert script_tag["src"] == "http://localhost:3000/static/src/entry.tsx" - assert script_tag["type"] == "module" - - -def test_vite_asset_raises_without_path(): - with pytest.raises(TemplateSyntaxError): - Template( - """ - {% load django_vite %} - {% vite_asset %} - """ - ) - - -def test_vite_react_refresh_happy_flow(): - template = Template( - """ - {% load django_vite %} - {% vite_react_refresh %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - script_tag = soup.script - assert not script_tag.has_attr("src") - assert script_tag.has_attr("type") - assert script_tag["type"] == "module" - assert "__vite_plugin_react_preamble_installed__" in script_tag.text - assert "http://localhost:3000/static/@react-refresh" in script_tag.text - - -def test_vite_react_refresh_returns_nothing_with_dev_mode_off(settings, monkeypatch): - settings.DJANGO_VITE_DEV_MODE = False - monkeypatch.setattr( - "django_vite.templatetags.django_vite.DJANGO_VITE_DEV_MODE", - settings.DJANGO_VITE_DEV_MODE, - ) - template = Template( - """ - {% load django_vite %} - {% vite_react_refresh %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - assert str(soup).strip() == "" - - -def test_vite_react_refresh_url_setting(override_setting): - override_setting("DJANGO_VITE_REACT_REFRESH_URL", "foobar") - template = Template( - """ - {% load django_vite %} - {% vite_react_refresh %} - """ - ) - html = template.render(Context({})) - soup = BeautifulSoup(html, "html.parser") - script_tag = soup.script - assert "http://localhost:3000/static/foobar" in script_tag.text diff --git a/tests/tests/templatetags/test_vite_asset.py b/tests/tests/templatetags/test_vite_asset.py new file mode 100644 index 0000000..7923954 --- /dev/null +++ b/tests/tests/templatetags/test_vite_asset.py @@ -0,0 +1,124 @@ +import pytest +from bs4 import BeautifulSoup +from django.template import Context, Template, TemplateSyntaxError +from django_vite.exceptions import DjangoViteAssetNotFoundError + + +def test_vite_asset_returns_dev_tags(): + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/entry.ts" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "http://localhost:3000/static/src/entry.ts" + assert script_tag["type"] == "module" + + +@pytest.mark.usefixtures("dev_mode_off") +def test_vite_asset_returns_production_tags(): + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/entry.ts" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "assets/entry-29e38a60.js" + assert script_tag["type"] == "module" + links = soup.find_all("link") + assert len(links) == 13 + + +def test_vite_asset_raises_without_path(): + with pytest.raises(TemplateSyntaxError): + Template( + """ + {% load django_vite %} + {% vite_asset %} + """ + ) + + +@pytest.mark.usefixtures("dev_mode_off") +def test_vite_asset_raises_nonexistent_entry(): + with pytest.raises(DjangoViteAssetNotFoundError): + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/fake.ts" %} + """ + ) + template.render(Context({})) + + +@pytest.mark.parametrize("prefix", ["custom/prefix", "custom/prefix/"]) +def test_vite_asset_dev_prefix(prefix, patch_settings): + patch_settings( + { + "DJANGO_VITE_STATIC_URL_PREFIX": prefix, + } + ) + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/entry.ts" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert ( + script_tag["src"] == "http://localhost:3000/static/custom/prefix/src/entry.ts" + ) + assert script_tag["type"] == "module" + + +@pytest.mark.usefixtures("dev_mode_off") +@pytest.mark.parametrize("prefix", ["custom/prefix", "custom/prefix/"]) +def test_vite_asset_production_prefix(prefix, patch_settings): + patch_settings( + { + "DJANGO_VITE_STATIC_URL_PREFIX": prefix, + } + ) + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/entry.ts" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "custom/prefix/assets/entry-29e38a60.js" + assert script_tag["type"] == "module" + links = soup.find_all("link") + assert len(links) == 13 + + +@pytest.mark.usefixtures("dev_mode_off") +def test_vite_asset_production_staticfiles_storage(patch_settings): + patch_settings( + { + "INSTALLED_APPS": ["django_vite", "django.contrib.staticfiles"], + } + ) + template = Template( + """ + {% load django_vite %} + {% vite_asset "src/entry.ts" %} + """ + ) + html = template.render(Context({})) + soup = BeautifulSoup(html, "html.parser") + script_tag = soup.find("script") + assert script_tag["src"] == "/static/assets/entry-29e38a60.js" + assert script_tag["type"] == "module" + links = soup.find_all("link") + assert len(links) == 13 diff --git a/tests/tests/templatetags/test_vite_asset_url.py b/tests/tests/templatetags/test_vite_asset_url.py new file mode 100644 index 0000000..e0eef07 --- /dev/null +++ b/tests/tests/templatetags/test_vite_asset_url.py @@ -0,0 +1,43 @@ +import pytest +from bs4 import BeautifulSoup +from django.template import Context, Template +from django_vite.exceptions import DjangoViteAssetNotFoundError + + +def test_vite_asset_url_returns_dev_url(): + template = Template( + """ + {% load django_vite %} +