From 06e622d8177509f26d49037a4e39566b83e1291b Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Thu, 21 May 2026 12:04:53 +0100 Subject: [PATCH] Refresh provisional results in place --- templates/results.html | 42 ++++++++++++++-- tests/test_app.py | 2 + tests/test_playwright_return_fares.py | 69 +++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/templates/results.html b/templates/results.html index 711ee2e..96b2e8a 100644 --- a/templates/results.html +++ b/templates/results.html @@ -262,8 +262,8 @@ if (currentNrClass === 'walkon') return row.walkon; var sectionFares = ADVANCE_FARES && ADVANCE_FARES[row.section]; var advFares = sectionFares && row.advance_key ? sectionFares[row.advance_key] : null; - if (!advFares) return null; - return currentNrClass === 'advance_std' ? advFares.advance_std : advFares.advance_1st; + if (!advFares) return row.walkon; + return (currentNrClass === 'advance_std' ? advFares.advance_std : advFares.advance_1st) || row.walkon; } function updateDisplay() { @@ -342,6 +342,42 @@ startTimetableRefresh(); } + function runInsertedScripts(root) { + root.querySelectorAll('script').forEach(function(oldScript) { + var script = document.createElement('script'); + for (var i = 0; i < oldScript.attributes.length; i++) { + var attr = oldScript.attributes[i]; + script.setAttribute(attr.name, attr.value); + } + script.text = oldScript.text; + oldScript.parentNode.replaceChild(script, oldScript); + }); + } + + function fullResultsUrl() { + var url = new URL(window.location.href); + url.searchParams.set('render', 'full'); + return url.toString(); + } + + function refreshFullResults() { + fetch(fullResultsUrl(), {headers: {'X-Requested-With': 'fetch'}}) + .then(function(response) { + if (!response.ok) throw new Error('Could not refresh results'); + return response.text(); + }) + .then(function(html) { + var doc = new DOMParser().parseFromString(html, 'text/html'); + var nextMain = doc.querySelector('main'); + var currentMain = document.querySelector('main'); + if (!nextMain || !currentMain) throw new Error('Results page was incomplete'); + document.title = doc.title; + currentMain.innerHTML = nextMain.innerHTML; + runInsertedScripts(currentMain); + }) + .catch(function() {}); + } + function startTimetableRefresh() { if (!HAS_PROVISIONAL_TIMETABLE || !TIMETABLE_REFRESH_URL || !window.EventSource) return; var source = new EventSource(TIMETABLE_REFRESH_URL); @@ -349,7 +385,7 @@ var msg = JSON.parse(event.data); if (msg.type === 'reload') { source.close(); - window.location.reload(); + refreshFullResults(); } else if (msg.type === 'eurostar_prices') { mergeEurostarPrices(msg.prices); updateDisplay(); diff --git a/tests/test_app.py b/tests/test_app.py index b36ec8e..b13287a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -139,6 +139,8 @@ def test_results_can_render_from_weekday_timetable_cache(monkeypatch): assert 'ES 9014' in html assert 'checking exact timetable' in html assert '/api/results_refresh/BRI/paris/2026-06-22' in html + assert 'refreshFullResults()' in html + assert 'window.location.reload()' not in html def test_results_refresh_reloads_when_exact_timetable_differs(monkeypatch): diff --git a/tests/test_playwright_return_fares.py b/tests/test_playwright_return_fares.py index 192b279..ee6e0d8 100644 --- a/tests/test_playwright_return_fares.py +++ b/tests/test_playwright_return_fares.py @@ -372,6 +372,75 @@ def test_single_advance_standard_premier_totals_on_initial_url(single_server): browser.close() +def test_single_advance_first_falls_back_to_walkon_when_unavailable(monkeypatch): + monkeypatch.setattr(app_module, "get_cached", lambda key, ttl=None: None) + monkeypatch.setattr(app_module, "set_cached", lambda key, data: None) + monkeypatch.setattr( + app_module.rtt_scraper, + "fetch", + lambda travel_date, user_agent, station_crs="BRI": [ + {"depart_bristol": "07:00", "arrive_paddington": "08:45", "headcode": "1A23"}, + ], + ) + monkeypatch.setattr( + app_module.gwr_fares_scraper, + "fetch", + lambda station_crs, travel_date: { + "07:00": {"ticket": "Anytime Day Single", "price": 138.70, "code": "SDS"}, + }, + ) + advance_fares = { + "07:00": { + "advance_std": {"ticket": "Advance Single", "price": 50.0, "code": "ADV"}, + "advance_1st": None, + }, + } + monkeypatch.setattr(app_module.gwr_fares_scraper, "fetch_advance", lambda station_crs, travel_date: advance_fares) + monkeypatch.setattr(app_module.gwr_fares_scraper, "fetch_advance_streaming", lambda station_crs, travel_date: iter([advance_fares])) + monkeypatch.setattr( + app_module.eurostar_scraper, + "fetch", + lambda destination, travel_date: [ + { + "depart_st_pancras": "10:01", + "arrive_destination": "13:34", + "destination": destination, + "train_number": "ES 9014", + "price": 59, + "seats": 42, + "plus_price": 89, + "plus_seats": 5, + }, + ], + ) + app_module.app.config["TESTING"] = True + server = make_server("127.0.0.1", 0, app_module.app) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with sync_playwright() as p: + browser = _launch_browser(p) + page = browser.new_page() + page.goto( + f"http://127.0.0.1:{server.server_port}" + "/results/BRI/paris/2026-07-20?nr_class=advance_1st&es_class=standard", + wait_until="domcontentloaded", + ) + + page.wait_for_function( + "Array.from(document.querySelectorAll('.total-price'))" + ".some(el => el.textContent.includes('£200.80'))", + timeout=10000, + ) + totals = [el.inner_text() for el in page.locator(".total-price").all()] + assert totals == ["£200.80"] + assert "No NR fare" not in " ".join(totals) + browser.close() + finally: + server.shutdown() + thread.join(timeout=5) + + def test_return_advance_first_standard_premier_totals(local_server): with sync_playwright() as p: browser = _launch_browser(p)