diff --git a/django_nepkit/tests/test_conf.py b/django_nepkit/tests/test_conf.py
new file mode 100644
index 0000000..b563eb9
--- /dev/null
+++ b/django_nepkit/tests/test_conf.py
@@ -0,0 +1,88 @@
+"""
+Tests for django-nepkit configuration (conf.py).
+"""
+
+import pytest
+
+
+class TestNepkitSettings:
+ """Tests for NepkitSettings configuration handling."""
+
+ def test_default_language_is_english(self):
+ """Default language should be 'en'."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ assert settings.DEFAULT_LANGUAGE == "en"
+
+ def test_default_date_formats(self):
+ """Default date input formats should be present."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ assert "%Y-%m-%d" in settings.DATE_INPUT_FORMATS
+ assert "%d/%m/%Y" in settings.DATE_INPUT_FORMATS
+ assert "%d-%m-%Y" in settings.DATE_INPUT_FORMATS
+
+ def test_default_bs_date_format(self):
+ """Default BS date format should be '%Y-%m-%d'."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ assert settings.BS_DATE_FORMAT == "%Y-%m-%d"
+
+ def test_default_bs_datetime_format(self):
+ """Default BS datetime format should include time."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ assert settings.BS_DATETIME_FORMAT == "%Y-%m-%d %H:%M:%S"
+
+ def test_default_time_format(self):
+ """Default time format should be 12-hour."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ assert settings.TIME_FORMAT == 12
+
+ def test_default_admin_datepicker(self):
+ """Admin datepicker should be enabled by default."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ assert settings.ADMIN_DATEPICKER is True
+
+ def test_user_setting_overrides_default(self):
+ """User settings should override defaults."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(
+ user_settings={"DEFAULT_LANGUAGE": "ne"}, defaults=DEFAULTS
+ )
+ assert settings.DEFAULT_LANGUAGE == "ne"
+
+ def test_invalid_setting_raises_attribute_error(self):
+ """Accessing an invalid setting key should raise AttributeError."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={}, defaults=DEFAULTS)
+ with pytest.raises(AttributeError, match="Invalid NEPKIT setting"):
+ _ = settings.NONEXISTENT_SETTING
+
+ def test_django_settings_override(self):
+ """nepkit_settings reads from Django's NEPKIT dict at import time."""
+ # The module-level nepkit_settings already uses NEPKIT from settings.py
+ from django_nepkit.conf import nepkit_settings
+
+ # Our test settings.py sets DEFAULT_LANGUAGE = "en"
+ assert nepkit_settings.DEFAULT_LANGUAGE == "en"
+
+ def test_user_setting_partial_override(self):
+ """Only explicitly set keys are overridden; others use defaults."""
+ from django_nepkit.conf import NepkitSettings, DEFAULTS
+
+ settings = NepkitSettings(user_settings={"TIME_FORMAT": 24}, defaults=DEFAULTS)
+ assert settings.TIME_FORMAT == 24
+ # Others still use defaults
+ assert settings.DEFAULT_LANGUAGE == "en"
+ assert settings.ADMIN_DATEPICKER is True
diff --git a/django_nepkit/tests/test_forms.py b/django_nepkit/tests/test_forms.py
index f1b1a99..dcfcb7a 100644
--- a/django_nepkit/tests/test_forms.py
+++ b/django_nepkit/tests/test_forms.py
@@ -50,7 +50,6 @@ class TestNepaliDateFormField:
with pytest.raises(ValidationError):
field.clean("")
- @pytest.mark.xfail(reason="Translation infrastructure timing issue in tests")
def test_invalid_date_format(self):
"""Test that invalid date format raises ValidationError."""
field = NepaliDateFormField()
diff --git a/django_nepkit/tests/test_lang_utils.py b/django_nepkit/tests/test_lang_utils.py
new file mode 100644
index 0000000..f7b03f9
--- /dev/null
+++ b/django_nepkit/tests/test_lang_utils.py
@@ -0,0 +1,132 @@
+"""
+Tests for django-nepkit language utilities (lang_utils.py).
+"""
+
+
+class TestResolveLanguageParams:
+ """Tests for resolve_language_params()."""
+
+ def test_defaults_to_english(self):
+ """With no args and default settings (en), ne=False and en=True."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params()
+ assert ne is False
+ assert en is True
+
+ def test_ne_true_sets_en_false(self):
+ """When ne=True is explicitly passed, en should be False."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params(ne=True)
+ assert ne is True
+ assert en is False
+
+ def test_en_true_sets_ne_false(self):
+ """When en=True is explicitly passed, ne should be False."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params(en=True)
+ assert ne is False
+ assert en is True
+
+ def test_explicit_en_true_with_ne_true(self):
+ """When both ne=True and en=True, en should respect the explicit value."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params(ne=True, en=True)
+ # ne=True with explicit en=True — en is explicitly set, so it stays True
+ assert ne is True
+ assert en is True
+
+ def test_ne_false_sets_en_true(self):
+ """When ne=False is explicitly passed, en should be True."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params(ne=False)
+ assert ne is False
+ assert en is True
+
+ def test_via_kwargs_ne(self):
+ """ne can be passed via **kwargs."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params(**{"ne": True})
+ assert ne is True
+ assert en is False
+
+ def test_via_kwargs_en(self):
+ """en can be passed via **kwargs."""
+ from django_nepkit.lang_utils import resolve_language_params
+
+ ne, en = resolve_language_params(**{"en": True})
+ assert ne is False
+ assert en is True
+
+
+class TestPopLanguageParams:
+ """Tests for pop_language_params()."""
+
+ def test_pops_ne_from_kwargs(self):
+ """ne is consumed from the kwargs dict."""
+ from django_nepkit.lang_utils import pop_language_params
+
+ kwargs = {"ne": True, "other": "value"}
+ ne, en = pop_language_params(kwargs)
+
+ assert ne is True
+ assert en is False
+ assert "ne" not in kwargs # was popped
+ assert "other" in kwargs # unrelated keys remain
+
+ def test_pops_en_from_kwargs(self):
+ """en is consumed from the kwargs dict."""
+ from django_nepkit.lang_utils import pop_language_params
+
+ kwargs = {"en": True, "other": "value"}
+ ne, en = pop_language_params(kwargs)
+
+ assert en is True
+ assert ne is False
+ assert "en" not in kwargs # was popped
+
+ def test_defaults_when_nothing_set(self):
+ """Without any language kwargs, defaults from settings are used."""
+ from django_nepkit.lang_utils import pop_language_params
+
+ kwargs = {}
+ ne, en = pop_language_params(kwargs)
+
+ # Default language is 'en' in test settings
+ assert ne is False
+ assert en is True
+
+ def test_ne_true_en_not_explicit(self):
+ """When ne=True is in kwargs and en is absent, en becomes False."""
+ from django_nepkit.lang_utils import pop_language_params
+
+ kwargs = {"ne": True}
+ ne, en = pop_language_params(kwargs)
+
+ assert ne is True
+ assert en is False
+
+ def test_ne_true_en_explicit_true(self):
+ """When both ne=True and en=True are in kwargs, en respects the explicit value."""
+ from django_nepkit.lang_utils import pop_language_params
+
+ kwargs = {"ne": True, "en": True}
+ ne, en = pop_language_params(kwargs)
+
+ assert ne is True
+ assert en is True
+
+ def test_extra_kwargs_not_touched(self):
+ """Other kwargs are left intact after popping language params."""
+ from django_nepkit.lang_utils import pop_language_params
+
+ kwargs = {"ne": False, "max_length": 100, "blank": True}
+ pop_language_params(kwargs)
+
+ assert "max_length" in kwargs
+ assert "blank" in kwargs
diff --git a/django_nepkit/tests/test_location_utils.py b/django_nepkit/tests/test_location_utils.py
new file mode 100644
index 0000000..2169052
--- /dev/null
+++ b/django_nepkit/tests/test_location_utils.py
@@ -0,0 +1,180 @@
+"""
+Tests for location utility helpers in utils.py:
+ - get_districts_by_province
+ - get_municipalities_by_district
+ - _normalize_nepali_text
+ - _matches_location_name
+ - _is_nepali_text
+"""
+
+
+class TestGetDistrictsByProvince:
+ """Tests for get_districts_by_province."""
+
+ def test_returns_districts_for_valid_province(self):
+ from django_nepkit.utils import get_districts_by_province
+
+ result = get_districts_by_province("Bagmati Province")
+ assert isinstance(result, list)
+ assert len(result) > 0
+ # Each item should have 'id' and 'text'
+ assert all("id" in item and "text" in item for item in result)
+
+ def test_includes_known_district(self):
+ from django_nepkit.utils import get_districts_by_province
+
+ result = get_districts_by_province("Bagmati Province")
+ district_names = [item["text"] for item in result]
+ assert "Kathmandu" in district_names
+
+ def test_returns_empty_for_invalid_province(self):
+ from django_nepkit.utils import get_districts_by_province
+
+ result = get_districts_by_province("Nonexistent Province")
+ assert result == []
+
+ def test_returns_nepali_names_when_ne_true(self):
+ from django_nepkit.utils import get_districts_by_province
+ import re
+
+ result = get_districts_by_province("Bagmati Province", ne=True)
+ assert len(result) > 0
+ has_devanagari = any(
+ re.search(r"[\u0900-\u097F]", item["text"]) for item in result
+ )
+ assert has_devanagari
+
+ def test_finds_province_by_nepali_name(self):
+ from django_nepkit.utils import get_districts_by_province
+
+ # बागमती प्रदेश is the Nepali name for Bagmati Province
+ result = get_districts_by_province("बागमती प्रदेश", ne=True)
+ assert len(result) > 0
+
+ def test_koshi_province(self):
+ from django_nepkit.utils import get_districts_by_province
+
+ result = get_districts_by_province("Koshi Province")
+ assert len(result) > 0
+ district_names = [item["text"] for item in result]
+ assert "Taplejung" in district_names or any(
+ "Taplejung" in n for n in district_names
+ )
+
+
+class TestGetMunicipalitiesByDistrict:
+ """Tests for get_municipalities_by_district."""
+
+ def test_returns_municipalities_for_valid_district(self):
+ from django_nepkit.utils import get_municipalities_by_district
+
+ result = get_municipalities_by_district("Kathmandu")
+ assert isinstance(result, list)
+ assert len(result) > 0
+ assert all("id" in item and "text" in item for item in result)
+
+ def test_includes_kathmandu_metro(self):
+ from django_nepkit.utils import get_municipalities_by_district
+
+ result = get_municipalities_by_district("Kathmandu")
+ names = [item["text"] for item in result]
+ assert any("Kathmandu" in n for n in names)
+
+ def test_returns_empty_for_invalid_district(self):
+ from django_nepkit.utils import get_municipalities_by_district
+
+ result = get_municipalities_by_district("Nonexistent District")
+ assert result == []
+
+ def test_returns_nepali_names_when_ne_true(self):
+ from django_nepkit.utils import get_municipalities_by_district
+ import re
+
+ result = get_municipalities_by_district("Kathmandu", ne=True)
+ assert len(result) > 0
+ has_devanagari = any(
+ re.search(r"[\u0900-\u097F]", item["text"]) for item in result
+ )
+ assert has_devanagari
+
+
+class TestNormalizeNepaliText:
+ """Tests for _normalize_nepali_text internal helper."""
+
+ def test_returns_none_for_falsy(self):
+ from django_nepkit.utils import _normalize_nepali_text
+
+ assert _normalize_nepali_text(None) is None
+ assert _normalize_nepali_text("") == ""
+
+ def test_replaces_chandrabindu_with_anusvara(self):
+ from django_nepkit.utils import _normalize_nepali_text
+
+ # ँ (chandrabindu) → ं (anusvara)
+ result = _normalize_nepali_text("काठमाडौँ")
+ assert "ँ" not in result
+ assert "ं" in result
+
+ def test_plain_text_unchanged(self):
+ from django_nepkit.utils import _normalize_nepali_text
+
+ text = "Kathmandu"
+ assert _normalize_nepali_text(text) == text
+
+
+class TestMatchesLocationName:
+ """Tests for _matches_location_name internal helper."""
+
+ def test_exact_nepali_match(self):
+ from django_nepkit.utils import _matches_location_name
+
+ assert _matches_location_name("Kathmandu", "काठमाडौं", "काठमाडौं", "काठमाडौं")
+
+ def test_exact_english_match_case_insensitive(self):
+ from django_nepkit.utils import _matches_location_name
+
+ assert _matches_location_name("Kathmandu", "काठमाडौं", "kathmandu", "kathmandu")
+
+ def test_partial_english_match(self):
+ from django_nepkit.utils import _matches_location_name
+
+ # "Pokhara" is contained in "Pokhara Metropolitan City"
+ assert _matches_location_name(
+ "Pokhara Metropolitan City", "पोखरा महानगरपालिका", "Pokhara", "Pokhara"
+ )
+
+ def test_short_token_no_partial_english_match(self):
+ from django_nepkit.utils import _matches_location_name
+
+ # Token shorter than 4 chars should not trigger partial English match
+ assert not _matches_location_name("Kathmandu", "काठमाडौं", "Kat", "Kat")
+
+ def test_no_match(self):
+ from django_nepkit.utils import _matches_location_name
+
+ assert not _matches_location_name("Lalitpur", "ललितपुर", "Pokhara", "Pokhara")
+
+
+class TestIsNepaliText:
+ """Tests for _is_nepali_text internal helper."""
+
+ def test_devanagari_detected(self):
+ from django_nepkit.utils import _is_nepali_text
+
+ assert _is_nepali_text(["काठमाडौं"])
+
+ def test_english_not_detected(self):
+ from django_nepkit.utils import _is_nepali_text
+
+ assert not _is_nepali_text(["Kathmandu", "Bagmati"])
+
+ def test_mixed_tokens(self):
+ from django_nepkit.utils import _is_nepali_text
+
+ # If any token has Devanagari, returns True
+ assert _is_nepali_text(["Kathmandu", "काठमाडौं"])
+
+ def test_empty_list(self):
+ from django_nepkit.utils import _is_nepali_text
+
+ assert not _is_nepali_text([])
diff --git a/django_nepkit/tests/test_views.py b/django_nepkit/tests/test_views.py
new file mode 100644
index 0000000..81a011d
--- /dev/null
+++ b/django_nepkit/tests/test_views.py
@@ -0,0 +1,274 @@
+"""
+Tests for django-nepkit views (views.py).
+Covers district_list_view, municipality_list_view, and private helpers.
+"""
+
+import pytest
+from django.test import RequestFactory
+
+
+@pytest.fixture
+def rf():
+ return RequestFactory()
+
+
+class TestRenderOptions:
+ """Tests for _render_options internal helper."""
+
+ def test_renders_placeholder_option(self):
+ from django_nepkit.views import _render_options
+
+ response = _render_options([], "Select District")
+ html = response.content.decode()
+ assert 'value=""' in html
+ assert "Select District" in html
+
+ def test_renders_data_options(self):
+ from django_nepkit.views import _render_options
+
+ data = [{"id": "Kathmandu", "text": "Kathmandu"}]
+ response = _render_options(data, "Select")
+ html = response.content.decode()
+ assert 'value="Kathmandu"' in html
+ assert ">Kathmandu<" in html
+
+ def test_renders_multiple_options(self):
+ from django_nepkit.views import _render_options
+
+ data = [
+ {"id": "Kathmandu", "text": "Kathmandu"},
+ {"id": "Lalitpur", "text": "Lalitpur"},
+ ]
+ response = _render_options(data, "Select")
+ html = response.content.decode()
+ assert "Kathmandu" in html
+ assert "Lalitpur" in html
+
+
+class TestGetPrimaryParam:
+ """Tests for _get_primary_param internal helper."""
+
+ def test_returns_named_param(self, rf):
+ from django_nepkit.views import _get_primary_param
+
+ request = rf.get("/", {"province": "Bagmati Province"})
+ value = _get_primary_param(request, "province")
+ assert value == "Bagmati Province"
+
+ def test_falls_back_to_first_non_internal_param(self, rf):
+ from django_nepkit.views import _get_primary_param
+
+ # No "province" key, but has "field" key (not an internal param)
+ request = rf.get("/", {"field": "Bagmati Province"})
+ value = _get_primary_param(request, "province")
+ assert value == "Bagmati Province"
+
+ def test_internal_params_excluded_from_fallback(self, rf):
+ from django_nepkit.views import _get_primary_param
+
+ # Only internal params (ne, en, html) present — should return None
+ request = rf.get("/", {"ne": "true", "en": "true", "html": "true"})
+ value = _get_primary_param(request, "province")
+ assert value is None
+
+ def test_returns_none_when_no_params(self, rf):
+ from django_nepkit.views import _get_primary_param
+
+ request = rf.get("/")
+ value = _get_primary_param(request, "province")
+ assert value is None
+
+
+class TestParseLanguageParams:
+ """Tests for _parse_language_params internal helper."""
+
+ def test_defaults_to_english(self, rf):
+ from django_nepkit.views import _parse_language_params
+
+ request = rf.get("/")
+ ne, en = _parse_language_params(request)
+ assert ne is False
+ assert en is True
+
+ def test_ne_true_from_query(self, rf):
+ from django_nepkit.views import _parse_language_params
+
+ request = rf.get("/", {"ne": "true"})
+ ne, en = _parse_language_params(request)
+ assert ne is True
+ assert en is False
+
+ def test_en_true_from_query(self, rf):
+ from django_nepkit.views import _parse_language_params
+
+ request = rf.get("/", {"en": "true"})
+ ne, en = _parse_language_params(request)
+ assert en is True
+
+ def test_ne_false_from_query(self, rf):
+ from django_nepkit.views import _parse_language_params
+
+ request = rf.get("/", {"ne": "false"})
+ ne, en = _parse_language_params(request)
+ assert ne is False
+ assert en is True
+
+
+class TestShouldReturnHtml:
+ """Tests for _should_return_html internal helper."""
+
+ def test_html_param_true(self, rf):
+ from django_nepkit.views import _should_return_html
+
+ request = rf.get("/", {"html": "true"})
+ assert _should_return_html(request) is True
+
+ def test_html_param_false(self, rf):
+ from django_nepkit.views import _should_return_html
+
+ request = rf.get("/", {"html": "false"})
+ assert _should_return_html(request) is False
+
+ def test_htmx_request_header(self, rf):
+ from django_nepkit.views import _should_return_html
+
+ request = rf.get("/", HTTP_HX_REQUEST="true")
+ assert _should_return_html(request) is True
+
+ def test_no_html_indicator(self, rf):
+ from django_nepkit.views import _should_return_html
+
+ request = rf.get("/")
+ assert _should_return_html(request) is False
+
+
+class TestDistrictListView:
+ """Tests for district_list_view."""
+
+ def test_returns_json_for_valid_province(self, rf):
+ from django_nepkit.views import district_list_view
+
+ request = rf.get("/", {"province": "Bagmati Province"})
+ response = district_list_view(request)
+
+ assert response.status_code == 200
+ import json
+
+ data = json.loads(response.content)
+ assert isinstance(data, list)
+ assert len(data) > 0
+ # Each item has id and text
+ assert "id" in data[0]
+ assert "text" in data[0]
+
+ def test_returns_empty_json_when_no_province(self, rf):
+ from django_nepkit.views import district_list_view
+
+ request = rf.get("/")
+ response = district_list_view(request)
+
+ assert response.status_code == 200
+ import json
+
+ data = json.loads(response.content)
+ assert data == []
+
+ def test_returns_html_when_html_true(self, rf):
+ from django_nepkit.views import district_list_view
+
+ request = rf.get("/", {"province": "Bagmati Province", "html": "true"})
+ response = district_list_view(request)
+
+ assert response.status_code == 200
+ assert "text/html" in response["Content-Type"]
+ assert b"