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)