Improve stop search and add documentation

This commit is contained in:
Edward Betts 2026-08-15 12:06:23 +01:00
parent 42484907ac
commit 71e1b57164
9 changed files with 358 additions and 37 deletions

View file

@ -25,9 +25,13 @@ def test_geocode_limits_search_to_great_britain_and_identifies_app() -> None:
json=[{"lat": "51.45", "lon": "-2.59", "display_name": "Bristol, England"}],
)
result = core.geocode("Bristol")
assert result == {"lat": 51.45, "lon": -2.59, "label": "Bristol, England"}
assert result == [{
"lat": 51.45, "lon": -2.59, "label": "Bristol, England", "type": None,
}]
request = responses.calls[0].request
assert "countrycodes=gb" in request.url
assert "limit=20" in request.url
assert "bounded=0" in request.url
assert request.headers["User-Agent"].startswith("uk-bus-stops/")
assert "edward@4angle.com" in request.headers["User-Agent"]
@ -143,6 +147,7 @@ def test_place_name_is_not_mistaken_for_atco_code() -> None:
"""Long alphabetic place names still go through Nominatim geocoding."""
assert ATCO_PATTERN.fullmatch("Manchester") is None
assert ATCO_PATTERN.fullmatch("0100BRP90314") is not None
assert ATCO_PATTERN.fullmatch("010000056") is not None
def test_parse_coordinates_accepts_valid_pair_and_rejects_invalid_pair() -> None:
@ -165,6 +170,45 @@ def test_search_coordinates_bypasses_nominatim(client: Any) -> None:
assert responses.calls[0].request.url.startswith(core.OVERPASS_URL)
@responses.activate
def test_geocode_uses_unbounded_map_bias() -> None:
"""The map view biases Nominatim ranking without restricting its results."""
responses.get(core.NOMINATIM_URL, json=[])
assert core.geocode("North Street", viewbox=(-2.7, 51.4, -2.5, 51.5)) == []
url = responses.calls[0].request.url
assert "viewbox=-2.7%2C51.5%2C-2.5%2C51.4" in url
assert "bounded=0" in url
@responses.activate
def test_place_search_returns_uk_choices_before_loading_stops(client: Any) -> None:
"""Ambiguous place searches return UK Nominatim choices without querying Overpass."""
responses.get(core.NOMINATIM_URL, json=[
{"lat": "51.1", "lon": "-1.3", "display_name": "North Street, Winchester",
"type": "residential"},
{"lat": "51.4", "lon": "-2.6", "display_name": "North Street, Bristol",
"type": "secondary"},
])
response = client.get(
"/api/search?q=North+Street&west=-2.7&south=51.4&east=-2.5&north=51.5"
)
assert response.status_code == 200
data = response.get_json()
assert data["kind"] == "geocode"
assert [item["label"] for item in data["locations"]] == [
"North Street, Winchester", "North Street, Bristol",
]
assert len(responses.calls) == 1
assert "viewbox=-2.7%2C51.5%2C-2.5%2C51.4" in responses.calls[0].request.url
def test_search_rejects_partial_viewbox(client: Any) -> None:
"""Search-bias bounds must contain all four valid coordinates."""
response = client.get("/api/search?q=North+Street&west=-2.7")
assert response.status_code == 400
assert response.get_json()["error"] == "invalid_viewbox"
def test_location_coordinates_are_validated(client: Any) -> None:
"""Latitude and longitude must be numeric and within geographic ranges."""
assert client.get("/api/stops?lat=hello&lon=1").status_code == 400
@ -211,3 +255,16 @@ def test_index_contains_search_and_map(client: Any) -> None:
assert b"Find a UK bus stop code" in response.data
assert b'id="search-input"' in response.data
assert b'id="map"' in response.data
assert b'href="/about"' in response.data
def test_about_documents_urls_and_services(client: Any) -> None:
"""The About page documents shareable links, APIs, and OSM data sources."""
response = client.get("/about")
assert response.status_code == 200
assert b"Shareable URL parameters" in response.data
assert b"?q=Bristol+Temple+Meads" in response.data
assert b"?node=485403178" in response.data
assert b"?lat=51.4545&lon=-2.5879" in response.data
assert b"overpass.atownsend.org.uk" in response.data
assert b"JSON endpoints" in response.data

View file

@ -39,7 +39,11 @@ def test_search_and_stop_details_in_browser(chromium_browser: Any, flask_url: st
"""A location search renders stops, an ATCO code, tags, and route metadata."""
page = chromium_browser.new_page(viewport={"width": 1280, "height": 800})
page_errors: list[str] = []
search_requests: list[str] = []
page.on("pageerror", lambda error: page_errors.append(str(error)))
page.on("request", lambda request: (
search_requests.append(request.url) if "/api/search?" in request.url else None
))
stop = {
"type": "node", "id": 123, "lat": 51.451, "lon": -2.591,
"name": "Central Stop", "atco_code": "0100BRP90314", "indicator": "N",
@ -48,14 +52,12 @@ def test_search_and_stop_details_in_browser(chromium_browser: Any, flask_url: st
"tags": {"highway": "bus_stop", "shelter": "yes"},
}
page.route("**/api/search?*", lambda route: route.fulfill(json={
"kind": "location",
"location": {"lat": 51.45, "lon": -2.59, "label": "Bristol"},
"stops": [{
"type": "node", "id": 125, "lat": 51.47, "lon": -2.61,
"name": "Far Stop", "atco_code": "0100BRP90316", "indicator": None,
"bearing": "E", "transport_type": "Bus stop",
"tags": {"highway": "bus_stop"},
}, stop],
"kind": "geocode",
"query": "Bristol",
"locations": [
{"lat": 51.45, "lon": -2.59, "label": "Bristol Centre", "type": "city"},
{"lat": 51.2, "lon": -2.7, "label": "Bristol Road, Somerset", "type": "road"},
],
}))
page.route("**/api/stops?*", lambda route: route.fulfill(json={
"kind": "location",
@ -85,8 +87,12 @@ def test_search_and_stop_details_in_browser(chromium_browser: Any, flask_url: st
page.get_by_label("Location or ATCO code").fill("Bristol")
page.get_by_role("button", name="Search").click()
playwright_api.expect(page).to_have_url(re.compile(r"\?q=Bristol$"))
assert all(key in search_requests[-1] for key in ("west=", "south=", "east=", "north="))
playwright_api.expect(page.get_by_text("2 UK matches for “Bristol”", exact=True)).to_be_visible()
page.get_by_text("Bristol Centre", exact=True).click()
playwright_api.expect(page).to_have_url(re.compile(r"\?lat=51.45&lon=-2.59$"))
playwright_api.expect(page.get_by_text("Central Stop", exact=True)).to_be_visible()
assert page.locator("#stop-list .stop-title").all_inner_texts() == ["Central Stop", "Far Stop"]
assert page.locator("#stop-list .stop-title").all_inner_texts() == ["Central Stop", "Nearby Stop"]
playwright_api.expect(page.locator("#stop-list .stop-meta").first).to_contain_text("m · Bus stop")
playwright_api.expect(page.locator("#stop-list .stop-meta").first).to_contain_text("NW-bound")
assert page.evaluate("map.getZoom()") > 6