Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ TODO: `obligatory_fields` is a new subsection, start using it in the actual appl
```
"accessible_by": ["bikes", "cars", "pedestrians"]
```
- `categories_default_checked` - for a given category, the options that should be pre-checked in the filter panel when the app first loads. E.g.
```json
"accessible_by": ["cars", "pedestrians"]
```
- `visible_data` - when a datapoint will be rendered as a pin on a map, these fields will be shown in the box when clicking on a pin. E.g.
```
"name",
Expand Down
40 changes: 40 additions & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,46 @@ Example configuration in your data source:
]
}

Categories and Filtering
~~~~~~~~~~~~~~~~~~~~~~~~

``categories`` is a dict of field name to the list of allowed values for that
field. Each category is rendered in the frontend as a group of filter
checkboxes, one per allowed value.

``categories_help``
List of category keys that should show a help tooltip next to the
category's title. The tooltip text is looked up via the translation key
``categories_help_<category_key>``.

``categories_options_help``
Dict of category key to the list of option values within that category
that should show a help tooltip. The tooltip text is looked up via the
translation key ``categories_options_help_<option_value>``.

``categories_default_checked``
Dict of category key to the list of option values that should be
pre-checked in the filter panel when the app first loads, before the user
has made any selection.

Example configuration in your data source:

.. code-block:: json

{
"categories": {
"accessible_by": ["bikes", "cars", "pedestrians"],
"type_of_place": ["big bridge", "small bridge"]
},
"categories_help": ["accessible_by"],
"categories_options_help": {
"accessible_by": ["cars", "pedestrians"]
},
"categories_default_checked": {
"accessible_by": ["cars"]
}
}

.. _data-model-visible_data:

Database Types
Expand Down
5 changes: 5 additions & 0 deletions e2e-tests/e2e_test_data_initial.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
"pedestrians"
]
},
"categories_default_checked": {
"accessible_by": [
"cars"
]
},
"visible_data": [
"remark",
"accessible_by",
Expand Down
7 changes: 7 additions & 0 deletions e2e-tests/tests/basic/test_accessibility_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ def setup(self, page: Page, geolocation):
# Navigate to the page
page.goto(BASE_URL, wait_until="domcontentloaded")

# "accessible_by: cars" is checked by default (see categories_default_checked
# in the test data), which excludes Zwierzyniecka (bikes/pedestrians only,
# no cars). Uncheck it so both seeded locations are visible, matching what
# these tests assert.
page.wait_for_selector("#filter-form", timeout=MARKER_LOAD_TIMEOUT)
page.locator("#filter-form input#cars").uncheck()

# Wait for map markers to load before clicking list view button
markers = page.locator(".leaflet-marker-icon")
expect(markers.first).to_be_visible(timeout=MARKER_LOAD_TIMEOUT)
Expand Down
6 changes: 5 additions & 1 deletion e2e-tests/tests/basic/test_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import pytest
from playwright.sync_api import Page, expect

from tests.conftest import BASE_URL, MARKER_LOAD_TIMEOUT
from tests.conftest import BASE_URL, MARKER_LOAD_TIMEOUT, clear_default_category_filters


def get_language_button(page: Page):
Expand Down Expand Up @@ -71,6 +71,10 @@ def test_switch_to_polish_changes_popup_text(self, page: Page):
page.get_by_role("link", name="polski").click()
page.wait_for_load_state("domcontentloaded")

# "accessible_by: cars" is checked by default, which excludes Zwierzyniecka
# (bikes/pedestrians only). Clear it so both seeded locations are visible.
clear_default_category_filters(page)

# Click marker cluster to expand
page.locator(".leaflet-marker-icon").first.click()

Expand Down
31 changes: 31 additions & 0 deletions e2e-tests/tests/basic/test_left_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,37 @@ def test_small_bridge_filter_has_helper_tooltip(self, page: Page):
expect(tooltip).to_contain_text("A smaller pedestrian or bike bridge")


class TestLeftPanelDefaultCheckedFilters:
"""Test suite for the categories_default_checked config option"""

def test_configured_option_is_checked_by_default(self, page: Page):
"""
The 'cars' option of the accessible_by category is configured as
default-checked (see categories_default_checked in the test data),
so it should be pre-checked when the filter form loads.
"""
page.set_viewport_size({"width": 1200, "height": 800})
page.goto(BASE_URL, wait_until="domcontentloaded")

page.wait_for_selector("#filter-form", timeout=10000)

cars_checkbox = page.locator("#filter-form input#cars")
expect(cars_checkbox).to_be_checked()

def test_other_options_are_not_checked_by_default(self, page: Page):
"""
Options not listed in categories_default_checked should remain
unchecked when the filter form loads.
"""
page.set_viewport_size({"width": 1200, "height": 800})
page.goto(BASE_URL, wait_until="domcontentloaded")

page.wait_for_selector("#filter-form", timeout=10000)

pedestrians_checkbox = page.locator("#filter-form input#pedestrians")
expect(pedestrians_checkbox).not_to_be_checked()


class TestLeftPanelScrollbar:
"""Test suite for panel scrollbar styling"""

Expand Down
12 changes: 8 additions & 4 deletions e2e-tests/tests/basic/test_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def test_should_not_have_scrollbars(self, page: Page):

def test_filter_checkbox_filters_markers(self, page: Page):
"""Verify clicking filter checkbox actually filters the markers on the map"""
# On desktop, filter panel is already visible (no toggle needed).
# "accessible_by: cars" is checked by default (see categories_default_checked
# in the test data), so start by clearing it to see both seeded locations.
cars_checkbox = page.get_by_role("checkbox", name="cars", exact=False)
expect(cars_checkbox).to_be_checked()
cars_checkbox.click()

# Wait for markers to load
first_marker = page.locator(".leaflet-marker-icon").first
expect(first_marker).to_be_visible(timeout=5000)
Expand All @@ -69,11 +76,8 @@ def test_filter_checkbox_filters_markers(self, page: Page):
markers = page.locator(".leaflet-marker-icon")
expect(markers).to_have_count(2)

# On desktop, filter panel is already visible (no toggle needed)

# Check the "cars" filter checkbox - this should filter to only show
# Re-check the "cars" filter checkbox - this should filter to only show
# places accessible by cars (1 marker instead of 2)
cars_checkbox = page.get_by_role("checkbox", name="cars", exact=False)
cars_checkbox.click()

# After filtering, only 1 marker should be visible (the one accessible by cars)
Expand Down
6 changes: 5 additions & 1 deletion e2e-tests/tests/basic/test_mobile_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pytest
from playwright.sync_api import Page, expect

from tests.conftest import ALL_MOBILE_DEVICES, BASE_URL
from tests.conftest import ALL_MOBILE_DEVICES, BASE_URL, clear_default_category_filters
from tests.helpers import EXPECTED_PLACE_ZWIERZYNIECKA, verify_popup_content, verify_problem_form


Expand All @@ -32,6 +32,10 @@ def test_displays_title_and_subtitle_in_popup(
# Navigate to the page (device emulation already configured by mobile_page fixture)
mobile_page.goto(BASE_URL, wait_until="domcontentloaded")

# "accessible_by: cars" is checked by default, which excludes Zwierzyniecka
# (bikes/pedestrians only). Clear it so both seeded locations are visible.
clear_default_category_filters(mobile_page)

# Click first marker to expand cluster
# Use JavaScript click to bypass webpack overlay that may intercept clicks on CI
first_marker = mobile_page.locator(".leaflet-marker-icon").first
Expand Down
10 changes: 9 additions & 1 deletion e2e-tests/tests/basic/test_popup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from playwright.sync_api import Page, expect

from tests.conftest import BASE_URL, MARKER_LOAD_TIMEOUT
from tests.conftest import BASE_URL, MARKER_LOAD_TIMEOUT, clear_default_category_filters
from tests.helpers import EXPECTED_PLACE_ZWIERZYNIECKA, verify_popup_content, verify_problem_form


Expand All @@ -22,6 +22,10 @@ def test_displays_popup_title_subtitle_categories_and_cta(self, page: Page, wind
"""
page.goto(BASE_URL, wait_until="domcontentloaded")

# "accessible_by: cars" is checked by default, which excludes Zwierzyniecka
# (bikes/pedestrians only). Clear it so both seeded locations are visible.
clear_default_category_filters(page)

# Click first marker to trigger cluster expansion
first_marker = page.locator(".leaflet-marker-icon").first
first_marker.click()
Expand Down Expand Up @@ -78,6 +82,10 @@ def test_problem_form_on_desktop(self, page: Page, window_open_stub):
"""
page.goto(BASE_URL, wait_until="domcontentloaded")

# "accessible_by: cars" is checked by default, which excludes Zwierzyniecka
# (bikes/pedestrians only). Clear it so both seeded locations are visible.
clear_default_category_filters(page)

# Click first marker to trigger cluster expansion
first_marker = page.locator(".leaflet-marker-icon").first
first_marker.click()
Expand Down
25 changes: 24 additions & 1 deletion e2e-tests/tests/basic/test_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
import pytest
from playwright.sync_api import Page, expect

from tests.conftest import ALL_MOBILE_DEVICES, BASE_URL, MARKER_LOAD_TIMEOUT
from tests.conftest import (
ALL_MOBILE_DEVICES,
BASE_URL,
MARKER_LOAD_TIMEOUT,
clear_default_category_filters,
)


class TestShareOnDesktop:
Expand All @@ -26,6 +31,10 @@ def test_share_button_copies_link_to_clipboard(self, page: Page):
# Grant clipboard permissions
page.context.grant_permissions(["clipboard-read", "clipboard-write"])

# "accessible_by: cars" is checked by default, which excludes Zwierzyniecka
# (bikes/pedestrians only). Clear it so both seeded locations are visible.
clear_default_category_filters(page)

# Click first marker to trigger cluster expansion
first_marker = page.locator(".leaflet-marker-icon").first
first_marker.click()
Expand Down Expand Up @@ -82,6 +91,11 @@ def test_shared_link_opens_popup_with_correct_content(self, page: Page):
wait_until="domcontentloaded",
)

# Zwierzyniecka (the shared location) isn't accessible by cars, so it's
# excluded by the "accessible_by: cars" default filter. Clear it so the
# marker renders and the pending shared-location popup can open.
clear_default_category_filters(page)

# Verify popup is visible
popup = page.locator(".leaflet-popup-content")
expect(popup).to_be_visible(timeout=MARKER_LOAD_TIMEOUT)
Expand Down Expand Up @@ -115,6 +129,10 @@ def test_share_button_triggers_native_share(self, mobile_page: Page, device_name

mobile_page.goto(BASE_URL, wait_until="domcontentloaded")

# "accessible_by: cars" is checked by default, which excludes Zwierzyniecka
# (bikes/pedestrians only). Clear it so both seeded locations are visible.
clear_default_category_filters(mobile_page)

# Click first marker to expand cluster
first_marker = mobile_page.locator(".leaflet-marker-icon").first
first_marker.evaluate("el => el.click()")
Expand Down Expand Up @@ -170,6 +188,11 @@ def test_shared_link_opens_popup_on_mobile(self, mobile_page: Page, device_name:
wait_until="domcontentloaded",
)

# Zwierzyniecka (the shared location) isn't accessible by cars, so it's
# excluded by the "accessible_by: cars" default filter. Clear it so the
# marker renders and the pending shared-location popup can open.
clear_default_category_filters(mobile_page)

# On mobile, popup appears as Material-UI Dialog
dialog_content = mobile_page.locator(".MuiDialogContent-root")
expect(dialog_content).to_be_visible(timeout=MARKER_LOAD_TIMEOUT)
Expand Down
24 changes: 24 additions & 0 deletions e2e-tests/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,30 @@
}


def clear_default_category_filters(page: Page) -> None:
"""
Uncheck the "cars" accessible_by filter that's checked by default (see
categories_default_checked in the e2e test data). Several tests rely on
both seeded locations (Grunwaldzki and Zwierzyniecka) being visible,
which requires clearing this default filter first.

Opens the mobile filter panel (if collapsed) to reach the checkbox, and
closes it again afterwards so it doesn't obscure subsequent interactions.
"""
toggle_button = page.locator('button[aria-label="Toggle left panel"]')
opened_dialog = toggle_button.is_visible()
if opened_dialog:
toggle_button.click()

page.wait_for_selector("#filter-form", timeout=MARKER_LOAD_TIMEOUT)
cars_checkbox = page.locator("#filter-form input#cars")
if cars_checkbox.is_checked():
cars_checkbox.uncheck()

if opened_dialog:
page.locator('button[aria-label="Close left panel"]').click()


def _block_hmr(page: Page) -> None:
"""Block HMR/hot reload requests to prevent page refreshes during tests."""
page.route("**/ws", lambda route: route.abort())
Expand Down
5 changes: 5 additions & 0 deletions examples/e2e_test_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@
"pedestrians"
]
},
"categories_default_checked": {
"accessible_by": [
"cars"
]
},
"visible_data": [
"remark",
"accessible_by",
Expand Down
15 changes: 11 additions & 4 deletions frontend/src/components/Categories/CategoriesContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ CategoriesContext.displayName = 'CategoriesContext';
* @returns {React.ReactElement} Context provider with categories state
*/
export const CategoriesProvider = ({ children }) => {
const [categories, setCategories] = useState([]);
const value = useMemo(() => ({ categories, setCategories }), [categories]);
const [categories, setCategories] = useState({});
const [isInitialized, setIsInitialized] = useState(false);
const value = useMemo(
() => ({ categories, setCategories, isInitialized, setIsInitialized }),
[categories, isInitialized],
);

return <CategoriesContext.Provider value={value}>{children}</CategoriesContext.Provider>;
};
Expand All @@ -27,9 +31,12 @@ export const CategoriesProvider = ({ children }) => {
* Must be used within a CategoriesProvider component.
*
* @throws {Error} If used outside of CategoriesProvider
* @returns {Object} Object containing categories array and setCategories function
* @returns {Array} return.categories - Current categories data
* @returns {Object} Object containing categories map and setCategories function
* @returns {Object} return.categories - Currently selected filter values keyed by category
* @returns {Function} return.setCategories - Function to update categories
* @returns {boolean} return.isInitialized - True once initial filter state (including
* default-checked options) has been loaded, so consumers can wait before fetching
* @returns {Function} return.setIsInitialized - Marks the initial filter state as loaded
*/
export const useCategories = () => {
const context = useContext(CategoriesContext);
Expand Down
Loading
Loading