Browser picker - offer each browser once, not twice

On any upgraded install update_35 has migrated the old per-engine request timeout and
User-Agent into browser configs keyed by the engine name ('html_requests',
'html_webdriver'), so those ids exist in BOTH the built-in engine list and browsers.json.
list_watch_browser_choices() concatenated the two sources, so the watch edit page rendered
the same browser as two radios with the same label - and the group override select did the
same, from its own copy of that concatenation.

Deduplicated by value at the source, keeping each value's first position (built-ins stay in
engine order) with its last label - the saved one, since it is the same browser under the
name the user can actually change. The group override select and the Add-Watch list now
derive from that single list instead of rebuilding or re-deduplicating it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dgtlmoon
2026-09-17 20:15:20 +02:00
co-authored by Claude Opus 5
parent ba2280ba81
commit e4c75bf486
4 changed files with 49 additions and 22 deletions
@@ -1008,3 +1008,33 @@ def test_update_36_survives_an_unusable_legacy_endpoint(client, live_server, mea
assert datastore.browser_config_store.get('extra_browser_Broken') is None
assert datastore.browser_config_store.get('extra_browser_Good')['browser_config']['connection_url'] \
== 'wss://good.example:9222'
def test_browser_picker_lists_each_browser_once(client, live_server, measure_memory_usage, datastore_path):
"""A saved config that shares a built-in engine's id must not appear twice in the picker.
update_35 migrates the old per-engine request timeout / User-Agent into configs keyed
'html_requests' / 'html_webdriver', so on any upgraded install those ids exist in BOTH the
built-in engine list and browsers.json - which rendered the same browser as two radios on the
watch edit page (and in the group override select).
"""
from changedetectionio.model.browser_config import list_watch_browser_choices
datastore = client.application.config.get('DATASTORE')
# Exactly what an upgraded install looks like after update_35
datastore.browser_config_store.upsert('html_requests', label='Basic fast Plaintext/HTTP Client',
base_fetcher='html_requests', browser_config={'timeout': 33})
values = [value for value, _label in list_watch_browser_choices(datastore)]
assert len(values) == len(set(values)), f"each browser must be offered once: {values}"
assert values.count('html_requests') == 1
# ...and the watch edit page renders one radio per browser
uuid = datastore.add_watch(url="https://example.com")
res = client.get(url_for("ui.ui_edit.edit_page", uuid=uuid))
assert res.status_code == 200
assert res.data.count(b'name="fetch_backend" type="radio" value="html_requests"') == 1
# A renamed config wins the label, since it is the same browser under a name the user chose
datastore.browser_config_store.update('html_requests', label='Fast plain client')
assert dict(list_watch_browser_choices(datastore))['html_requests'] == 'Fast plain client'