feat: add click-to-deselect behavior and update checked-radio serialization in JS
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
"""End-to-end Playwright test for boolean radio filter serialization and deselect behavior.
|
||||||
|
|
||||||
|
Covers:
|
||||||
|
1. Selecting True/False serializes the boolean field as True/False.
|
||||||
|
2. Unsetting/unchecking a radio button by clicking on it again, which deselects it, omitting the field from JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from common.components import FilterBar
|
||||||
|
|
||||||
|
|
||||||
|
def _bar_page(filter_json: str = "") -> str:
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Boolean filter E2E</title>
|
||||||
|
<script src="/static/js/range_slider.js" defer></script>
|
||||||
|
<script src="/static/js/search_select.js" defer></script>
|
||||||
|
<script src="/static/js/filter_bar.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{FilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")}
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def empty_bar_view(request):
|
||||||
|
return HttpResponse(_bar_page())
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("test-boolean-filter/", empty_bar_view),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_from_url(url: str) -> dict:
|
||||||
|
"""Extract and parse the ?filter=... query param from a URL."""
|
||||||
|
query = urllib.parse.urlparse(url).query
|
||||||
|
params = urllib.parse.parse_qs(query)
|
||||||
|
raw = params.get("filter", [""])[0]
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(ROOT_URLCONF="e2e.test_boolean_filter_e2e")
|
||||||
|
def test_no_selection_omits_boolean_filters(live_server, page):
|
||||||
|
page.goto(live_server.url + "/test-boolean-filter/")
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.evaluate(
|
||||||
|
"document.getElementById('filter-bar-form')"
|
||||||
|
".dispatchEvent(new Event('submit', {cancelable: true}))"
|
||||||
|
)
|
||||||
|
parsed = _filter_from_url(page.url)
|
||||||
|
assert "mastered" not in parsed
|
||||||
|
assert "purchase_refunded" not in parsed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(ROOT_URLCONF="e2e.test_boolean_filter_e2e")
|
||||||
|
def test_select_true_and_false_serializes_correctly(live_server, page):
|
||||||
|
page.goto(live_server.url + "/test-boolean-filter/")
|
||||||
|
|
||||||
|
# Select "True" for Mastered
|
||||||
|
# Under PurchaseFilterBar: "filter-mastered" is the mastered radio name.
|
||||||
|
# The true radio has value="true", false radio has value="false"
|
||||||
|
true_radio = page.locator('input[name="filter-mastered"][value="true"]')
|
||||||
|
true_radio.click()
|
||||||
|
|
||||||
|
# Select "False" for Refunded (filter-purchase-refunded)
|
||||||
|
false_radio = page.locator('input[name="filter-purchase-refunded"][value="false"]')
|
||||||
|
false_radio.click()
|
||||||
|
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.evaluate(
|
||||||
|
"document.getElementById('filter-bar-form')"
|
||||||
|
".dispatchEvent(new Event('submit', {cancelable: true}))"
|
||||||
|
)
|
||||||
|
parsed = _filter_from_url(page.url)
|
||||||
|
assert parsed.get("mastered") == {"value": True, "modifier": "EQUALS"}
|
||||||
|
assert parsed.get("purchase_refunded") == {"value": False, "modifier": "EQUALS"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@override_settings(ROOT_URLCONF="e2e.test_boolean_filter_e2e")
|
||||||
|
def test_click_to_deselect_radio_works(live_server, page):
|
||||||
|
page.goto(live_server.url + "/test-boolean-filter/")
|
||||||
|
|
||||||
|
true_radio = page.locator('input[name="filter-mastered"][value="true"]')
|
||||||
|
|
||||||
|
# First click checks it
|
||||||
|
true_radio.click()
|
||||||
|
assert true_radio.is_checked()
|
||||||
|
|
||||||
|
# Second click deselects it
|
||||||
|
true_radio.click()
|
||||||
|
assert not true_radio.is_checked()
|
||||||
|
|
||||||
|
with page.expect_navigation():
|
||||||
|
page.evaluate(
|
||||||
|
"document.getElementById('filter-bar-form')"
|
||||||
|
".dispatchEvent(new Event('submit', {cancelable: true}))"
|
||||||
|
)
|
||||||
|
parsed = _filter_from_url(page.url)
|
||||||
|
assert "mastered" not in parsed
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Boolean Fields (Checkboxes)
|
// 2. Boolean Fields (Radio Button Groups)
|
||||||
var booleanFields = [
|
var booleanFields = [
|
||||||
{ name: "filter-mastered", key: "mastered" },
|
{ name: "filter-mastered", key: "mastered" },
|
||||||
{ name: "filter-emulated", key: "emulated" },
|
{ name: "filter-emulated", key: "emulated" },
|
||||||
@@ -127,9 +127,10 @@
|
|||||||
{ name: "filter-session-emulated", key: "session_emulated" }
|
{ name: "filter-session-emulated", key: "session_emulated" }
|
||||||
];
|
];
|
||||||
booleanFields.forEach(function (bf) {
|
booleanFields.forEach(function (bf) {
|
||||||
var el = form.querySelector('[name="' + bf.name + '"]');
|
var el = form.querySelector('[name="' + bf.name + '"]:checked');
|
||||||
if (el && el.checked) {
|
if (el) {
|
||||||
filter[bf.key] = criterion(true, null, "EQUALS");
|
var val = el.value === "true";
|
||||||
|
filter[bf.key] = criterion(val, null, "EQUALS");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -400,8 +401,36 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable deselect-on-click behavior for filter radio buttons.
|
||||||
|
*/
|
||||||
|
function setupDeselectableRadios() {
|
||||||
|
document.querySelectorAll('input[type="radio"]').forEach(function (radio) {
|
||||||
|
radio.addEventListener('click', function (e) {
|
||||||
|
if (this.wasChecked) {
|
||||||
|
this.checked = false;
|
||||||
|
this.wasChecked = false;
|
||||||
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
} else {
|
||||||
|
var name = this.getAttribute('name');
|
||||||
|
if (name) {
|
||||||
|
document.querySelectorAll('input[type="radio"][name="' + name + '"]').forEach(function (r) {
|
||||||
|
r.wasChecked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.wasChecked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (radio.checked) {
|
||||||
|
radio.wasChecked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
injectSearchInputs();
|
injectSearchInputs();
|
||||||
|
setupDeselectableRadios();
|
||||||
loadPresets();
|
loadPresets();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user