Improve progressive results loading

This commit is contained in:
Edward Betts 2026-05-21 09:52:58 +01:00
parent 9691632f65
commit 378d2484d0
6 changed files with 369 additions and 60 deletions

128
app.py
View file

@ -113,6 +113,35 @@ def _parse_connection(raw, default, valid_set):
return val if val in valid_set else default
def _results_url(
station_crs,
slug,
travel_date,
journey_type="outbound",
return_date=None,
**params,
):
params = {k: v for k, v in params.items() if v is not None}
if journey_type == "return":
return url_for(
"return_results",
station_crs=station_crs,
slug=slug,
travel_date=travel_date,
return_date=return_date,
**params,
)
if journey_type == "inbound":
params["journey_type"] = "inbound"
return url_for(
"results",
station_crs=station_crs,
slug=slug,
travel_date=travel_date,
**params,
)
@app.route("/search")
def search():
slug = request.args.get("destination", "")
@ -150,12 +179,11 @@ def search():
return_date = ""
if slug in DESTINATIONS and travel_date and (journey_type != "return" or return_date):
return redirect(
url_for(
"results",
_results_url(
station_crs=station_crs,
slug=slug,
travel_date=travel_date,
journey_type=None if journey_type == "outbound" else journey_type,
journey_type=journey_type,
return_date=return_date if journey_type == "return" else None,
min_connection=None if min_conn == default_min else min_conn,
max_connection=None if max_conn == default_max else max_conn,
@ -168,6 +196,21 @@ def search():
@app.route("/results/<station_crs>/<slug>/<travel_date>")
def results(station_crs, slug, travel_date):
return _results(
station_crs,
slug,
travel_date,
request.args.get("journey_type", "outbound"),
request.args.get("return_date"),
)
@app.route("/results/<station_crs>/<slug>/<travel_date>/return/<return_date>")
def return_results(station_crs, slug, travel_date, return_date):
return _results(station_crs, slug, travel_date, "return", return_date)
def _results(station_crs, slug, travel_date, journey_type, return_date):
departure_station_name = STATION_BY_CRS.get(station_crs)
if departure_station_name is None:
abort(404)
@ -175,10 +218,8 @@ def results(station_crs, slug, travel_date):
if not destination or not travel_date:
return redirect(url_for("index"))
journey_type = request.args.get("journey_type", "outbound")
if journey_type not in VALID_JOURNEY_TYPES:
journey_type = "outbound"
return_date = request.args.get("return_date")
if journey_type == "return":
try:
if not return_date or date.fromisoformat(return_date) < date.fromisoformat(travel_date):
@ -205,6 +246,38 @@ def results(station_crs, slug, travel_date):
if es_class not in VALID_ES_CLASSES:
es_class = DEFAULT_ES_CLASS
if (
request.args.get("render") != "full"
and not (
app.config.get("TESTING")
and request.args.get("progressive") != "1"
)
):
dt = date.fromisoformat(travel_date)
travel_date_display = dt.strftime("%A %-d %B %Y")
full_args = dict(request.args)
full_args.pop("progressive", None)
full_args.pop("journey_type", None)
full_args.pop("return_date", None)
full_args["render"] = "full"
return render_template(
"results_loading.html",
destination=destination,
departure_station_name=departure_station_name,
journey_type=journey_type,
travel_date_display=travel_date_display,
return_date=return_date,
full_results_url=_results_url(
station_crs=station_crs,
slug=slug,
travel_date=travel_date,
journey_type=journey_type,
return_date=return_date,
**full_args,
),
index_url=url_for("index"),
)
user_agent = request.headers.get("User-Agent", rtt_scraper.DEFAULT_UA)
error_messages = []
from_cache_parts = []
@ -413,6 +486,46 @@ def results(station_crs, slug, travel_date):
url_max = None if max_connection == default_max else max_connection
url_nr = None if nr_class == DEFAULT_NR_CLASS else nr_class
url_es = None if es_class == DEFAULT_ES_CLASS else es_class
common_url_args = {
"journey_type": journey_type,
"return_date": return_date,
"min_connection": url_min,
"max_connection": url_max,
"nr_class": url_nr,
"es_class": url_es,
}
prev_results_url = _results_url(
station_crs,
slug,
prev_date,
**common_url_args,
)
next_results_url = _results_url(
station_crs,
slug,
next_date,
**common_url_args,
)
destination_links = [
(
destination_slug,
destination_name,
_results_url(
station_crs,
destination_slug,
travel_date,
**common_url_args,
),
)
for destination_slug, destination_name in DESTINATIONS.items()
]
results_base_url = _results_url(
station_crs,
slug,
travel_date,
journey_type=journey_type,
return_date=return_date,
)
trip_fares = {}
advance_fares = {}
@ -465,6 +578,10 @@ def results(station_crs, slug, travel_date):
departure_station_name=departure_station_name,
prev_date=prev_date,
next_date=next_date,
prev_results_url=prev_results_url,
next_results_url=next_results_url,
destination_links=destination_links,
results_base_url=results_base_url,
travel_date_display=travel_date_display,
gwr_count=sum(section["gwr_count"] for section in sections),
eurostar_count=sum(section["eurostar_count"] for section in sections),
@ -484,7 +601,6 @@ def results(station_crs, slug, travel_date):
es_class=es_class,
url_nr_class=url_nr,
url_es_class=url_es,
url_journey_type=None if journey_type == "outbound" else journey_type,
trip_fares_json=json.dumps(trip_fares),
advance_fares_json=json.dumps(advance_fares),
advance_api_urls_json=json.dumps(advance_api_urls),

View file

@ -294,6 +294,33 @@
/* Loading state */
#advance-loading { font-size: 0.82rem; color: #718096; margin-left: 0.5rem; }
.loading-panel {
display: flex;
gap: 1rem;
align-items: flex-start;
margin-top: 1rem;
padding: 1rem;
border: 1px solid #cbd5e0;
border-radius: 6px;
background: #f8fbff;
}
.loading-panel p { margin: 0.25rem 0 0; }
.spinner {
width: 1.5rem;
height: 1.5rem;
border: 3px solid #cbd5e0;
border-top-color: #00539f;
border-radius: 50%;
flex: 0 0 auto;
animation: spin 0.8s linear infinite;
}
.spinner-inline {
width: 0.85rem;
height: 0.85rem;
border-width: 2px;
margin-right: 0.35rem;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Fare lines — show all, dim inactive */
.fare-line { display: block; line-height: 1.6; transition: opacity 0.15s; }

View file

@ -21,22 +21,22 @@
{% endif %}
</h2>
<div class="date-nav">
<a href="{{ url_for('results', station_crs=station_crs, slug=slug, travel_date=prev_date, journey_type=url_journey_type, return_date=return_date, min_connection=url_min_connection, max_connection=url_max_connection, nr_class=url_nr_class, es_class=url_es_class) }}"
<a href="{{ prev_results_url }}"
class="btn-nav">&larr; Prev</a>
<strong>{{ travel_date_display }}{% if return_date %} to {{ return_date }}{% endif %}</strong>
<a href="{{ url_for('results', station_crs=station_crs, slug=slug, travel_date=next_date, journey_type=url_journey_type, return_date=return_date, min_connection=url_min_connection, max_connection=url_max_connection, nr_class=url_nr_class, es_class=url_es_class) }}"
<a href="{{ next_results_url }}"
class="btn-nav">Next &rarr;</a>
</div>
<div class="switcher-section">
<div class="section-label">Switch destination for {{ travel_date_display }}</div>
<div class="chip-row">
{% for destination_slug, destination_name in destinations.items() %}
{% for destination_slug, destination_name, destination_url in destination_links %}
{% if destination_slug == slug %}
<span class="chip-current">{{ destination_name }}</span>
{% else %}
<a
class="chip-link"
href="{{ url_for('results', station_crs=station_crs, slug=destination_slug, travel_date=travel_date, journey_type=url_journey_type, return_date=return_date, min_connection=url_min_connection, max_connection=url_max_connection, nr_class=url_nr_class, es_class=url_es_class) }}"
href="{{ destination_url }}"
>{{ destination_name }}</a>
{% endif %}
{% endfor %}
@ -70,6 +70,7 @@
<button type="button" class="btn-group-option {% if nr_class == 'advance_std' %}active{% endif %}" onclick="setNrClass('advance_std')">Advance Std</button>
<button type="button" class="btn-group-option {% if nr_class == 'advance_1st' %}active{% endif %}" onclick="setNrClass('advance_1st')">Advance 1st</button>
</div>
<span id="advance-loading" style="display:none"><span class="spinner spinner-inline" aria-hidden="true"></span>Loading advance fares</span>
</div>
<div>
<span class="filter-label">Eurostar:</span>
@ -81,9 +82,7 @@
</div>
</div>
<script>
const RESULTS_BASE = '{{ url_for('results', station_crs=station_crs, slug=slug, travel_date=travel_date) }}';
const JOURNEY_TYPE = '{{ journey_type }}';
const RETURN_DATE = '{{ return_date or '' }}';
const RESULTS_BASE = '{{ results_base_url }}';
const DEFAULT_MIN_CONN = {{ default_min_connection }};
const DEFAULT_MAX_CONN = {{ default_max_connection }};
let TRIP_FARES = {{ trip_fares_json | safe }};
@ -95,12 +94,18 @@
let currentEsClass = '{{ es_class }}';
let advanceLoadingSections = {};
function updateAdvanceLoadingStatus() {
var loading = Object.keys(advanceLoadingSections).some(function(sectionId) {
return advanceLoadingSections[sectionId];
});
var el = document.getElementById('advance-loading');
if (el) el.style.display = loading ? 'inline-flex' : 'none';
}
function buildUrl(nrCls, esCls) {
var min = parseInt(document.getElementById('min_conn_select').value);
var max = parseInt(document.getElementById('max_conn_select').value);
var params = [];
if (JOURNEY_TYPE !== 'outbound') params.push('journey_type=' + encodeURIComponent(JOURNEY_TYPE));
if (RETURN_DATE) params.push('return_date=' + encodeURIComponent(RETURN_DATE));
if (min !== DEFAULT_MIN_CONN) params.push('min_connection=' + min);
if (max !== DEFAULT_MAX_CONN) params.push('max_connection=' + max);
if (nrCls !== 'walkon') params.push('nr_class=' + nrCls);
@ -169,6 +174,7 @@
function loadAdvanceFaresForSection(sectionId) {
if (advanceLoadingSections[sectionId] || !ADVANCE_API_URLS[sectionId]) return;
advanceLoadingSections[sectionId] = true;
updateAdvanceLoadingStatus();
if (!ADVANCE_FARES) ADVANCE_FARES = {};
if (!ADVANCE_FARES[sectionId]) ADVANCE_FARES[sectionId] = {};
@ -183,6 +189,7 @@
.catch(function() {})
.finally(function() {
advanceLoadingSections[sectionId] = false;
updateAdvanceLoadingStatus();
updateDisplay();
});
}
@ -190,29 +197,41 @@
function loadAdvanceFaresForSectionStreaming(sectionId) {
if (advanceLoadingSections[sectionId] || !ADVANCE_STREAM_URLS[sectionId]) return;
advanceLoadingSections[sectionId] = true;
updateAdvanceLoadingStatus();
if (!ADVANCE_FARES) ADVANCE_FARES = {};
if (!ADVANCE_FARES[sectionId]) ADVANCE_FARES[sectionId] = {};
var hadMessage = false;
var source = new EventSource(ADVANCE_STREAM_URLS[sectionId]);
source.onmessage = function(event) {
hadMessage = true;
var msg = JSON.parse(event.data);
if (msg.type === 'fares') mergeAdvanceFares(sectionId, msg.fares);
if (msg.type === 'fares') {
mergeAdvanceFares(sectionId, msg.fares);
updateDisplay();
}
if (msg.type === 'done' || msg.type === 'error') {
advanceLoadingSections[sectionId] = false;
source.close();
updateAdvanceLoadingStatus();
updateDisplay();
}
};
source.onerror = function() {
advanceLoadingSections[sectionId] = false;
source.close();
updateDisplay();
updateAdvanceLoadingStatus();
if (!hadMessage && ADVANCE_API_URLS[sectionId]) {
loadAdvanceFaresForSection(sectionId);
} else {
updateDisplay();
}
};
}
function loadMissingAdvanceFares() {
for (var sectionId in ADVANCE_API_URLS) {
if (sectionNeedsAdvance(sectionId)) loadAdvanceFaresForSection(sectionId);
for (var sectionId in ADVANCE_STREAM_URLS) {
if (sectionNeedsAdvance(sectionId)) loadAdvanceFaresForSectionStreaming(sectionId);
}
}
@ -281,8 +300,8 @@
var total = totals[key];
var html = '<span class="text-sm text-green" style="font-weight:700">' + fmtPrice(total);
if (minTotal !== null && maxTotal !== null) {
if (total <= minTotal + 10) html += ' <span title="Cheapest journey">low</span>';
else if (total >= maxTotal - 10) html += ' <span title="Most expensive journey">high</span>';
if (total <= minTotal + 10) html += ' <span title="Cheapest journey">🪙</span>';
else if (total >= maxTotal - 10) html += ' <span title="Most expensive journey">💸</span>';
}
html += '</span>';
totalSpan.innerHTML = html;
@ -293,10 +312,16 @@
});
}
document.addEventListener('DOMContentLoaded', function() {
if (currentNrClass === 'advance_std' || currentNrClass === 'advance_1st') loadMissingAdvanceFares();
updateDisplay();
});
function initialiseResultsPage() {
if (currentNrClass === 'advance_std' || currentNrClass === 'advance_1st') loadMissingAdvanceFares();
updateDisplay();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialiseResultsPage);
} else {
initialiseResultsPage();
}
</script>
<p class="card-meta">
{{ gwr_count }} National Rail service{{ 's' if gwr_count != 1 }}
@ -382,10 +407,14 @@
<span class="fare-line es-plus">{% if row.eurostar_plus_price is not none %}<span class="text-sm font-bold">£{{ "%.2f"|format(row.eurostar_plus_price) }}</span>{% endif %}</span>
</td>
<td class="col-transfer" style="color:#4a5568">
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 45 %} <span title="Tight connection">!</span>{% endif %}</span>
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 45 %} <span title="Tight connection">⚠️</span>{% endif %}</span>
{% if row.circle_services %}
{% set c = row.circle_services[0] %}
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} &rarr; PAD {{ c.arrive_pad }} · £{{ "%.2f"|format(c.fare) }}</span>
{% if row.circle_services | length > 1 %}
{% set c2 = row.circle_services[1] %}
<br><span class="text-xs text-muted nowrap" style="opacity:0.7">next {{ c2.depart }} &rarr; PAD {{ c2.arrive_pad }} · £{{ "%.2f"|format(c2.fare) }}</span>
{% endif %}
{% endif %}
</td>
<td>
@ -410,10 +439,14 @@
<span class="fare-line nr-advance-1st"></span>
</td>
<td class="col-transfer" style="color:#4a5568">
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 80 %} <span title="Tight connection">!</span>{% endif %}</span>
<span class="nowrap">{{ row.connection_duration }}{% if row.connection_minutes < 80 %} <span title="Tight connection">⚠️</span>{% endif %}</span>
{% if row.circle_services %}
{% set c = row.circle_services[0] %}
<br><span class="text-xs text-muted nowrap">Circle {{ c.depart }} &rarr; KX {{ c.arrive_kx }} · £{{ "%.2f"|format(c.fare) }}</span>
{% if row.circle_services | length > 1 %}
{% set c2 = row.circle_services[1] %}
<br><span class="text-xs text-muted nowrap" style="opacity:0.7">next {{ c2.depart }} &rarr; KX {{ c2.arrive_kx }} · £{{ "%.2f"|format(c2.fare) }}</span>
{% endif %}
{% endif %}
</td>
<td>

View file

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block title %}{% if journey_type == 'inbound' %}{{ destination }} to {{ departure_station_name }} via Eurostar{% elif journey_type == 'return' %}{{ departure_station_name }} to {{ destination }} return via Eurostar{% else %}{{ departure_station_name }} to {{ destination }} via Eurostar{% endif %}{% endblock %}
{% block og_title %}{{ self.title()|trim }}{% endblock %}
{% block og_description %}Train options from {{ departure_station_name }} to {{ destination }} via Paddington, St Pancras, and Eurostar.{% endblock %}
{% block twitter_title %}{{ self.title()|trim }}{% endblock %}
{% block twitter_description %}Train options from {{ departure_station_name }} to {{ destination }} via Paddington, St Pancras, and Eurostar.{% endblock %}
{% block content %}
<p class="back-link">
<a href="{{ index_url }}">&larr; New search</a>
</p>
<div class="card" style="margin-bottom:1.5rem">
<h2>
{% if journey_type == 'inbound' %}
{{ destination }} &rarr; {{ departure_station_name }}
{% elif journey_type == 'return' %}
{{ departure_station_name }} &harr; {{ destination }}
{% else %}
{{ departure_station_name }} &rarr; {{ destination }}
{% endif %}
</h2>
<p class="card-meta">
{{ travel_date_display }}{% if return_date %} to {{ return_date }}{% endif %}
</p>
<div class="loading-panel" role="status" aria-live="polite">
<span class="spinner" aria-hidden="true"></span>
<div>
<strong>Loading train times and fares</strong>
<p class="text-muted text-sm">Fetching National Rail, Eurostar, and fare data. Results will appear here as soon as they are ready.</p>
</div>
</div>
<noscript>
<p><a href="{{ full_results_url }}">Load results</a></p>
</noscript>
</div>
<script>
(function() {
function runScripts(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);
});
}
fetch({{ full_results_url|tojson }}, {headers: {'X-Requested-With': 'fetch'}})
.then(function(response) {
if (!response.ok) throw new Error('Could not load results');
return response.text();
})
.then(function(html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
document.title = doc.title;
var nextMain = doc.querySelector('main');
var currentMain = document.querySelector('main');
if (!nextMain || !currentMain) throw new Error('Results page was incomplete');
currentMain.innerHTML = nextMain.innerHTML;
runScripts(currentMain);
history.replaceState(null, '', window.location.href);
})
.catch(function() {
var panel = document.querySelector('.loading-panel');
if (panel) {
panel.innerHTML = '<div><strong>Could not load results</strong><p class="text-muted text-sm"><a href="{{ full_results_url }}">Try loading the full results page</a>.</p></div>';
}
});
})();
</script>
{% endblock %}

View file

@ -1,4 +1,7 @@
from datetime import datetime
import app as app_module
import trip_planner as trip_planner_module
def _client():
@ -72,7 +75,7 @@ def test_search_redirects_return_with_return_date():
assert resp.status_code == 302
assert resp.headers['Location'].endswith(
'/results/BRI/paris/2026-04-10?journey_type=return&return_date=2026-04-17'
'/results/BRI/paris/2026-04-10/return/2026-04-17'
)
@ -91,6 +94,23 @@ def test_results_shows_same_day_destination_switcher(monkeypatch):
assert 'ES 9014' in html
def test_results_progressive_shell_loads_without_scraping(monkeypatch):
def fail_fetch(*args, **kwargs):
raise AssertionError("progressive shell should not fetch data")
monkeypatch.setattr(app_module.rtt_scraper, 'fetch', fail_fetch)
monkeypatch.setattr(app_module.eurostar_scraper, 'fetch', fail_fetch)
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', fail_fetch)
client = _client()
resp = client.get('/results/BRI/paris/2026-04-10?progressive=1')
html = resp.get_data(as_text=True)
assert resp.status_code == 200
assert 'Loading train times and fares' in html
assert 'render=full' in html
def test_results_title_and_social_meta_include_destination(monkeypatch):
_stub_data(monkeypatch)
client = _client()
@ -380,14 +400,39 @@ def test_results_return_renders_outbound_and_inbound_tables(monkeypatch):
],
},
)
monkeypatch.setattr(
trip_planner_module.circle_line,
'upcoming_services',
lambda earliest_board, count=2, direction='pad_to_kx': (
[
(datetime(2026, 4, 10, 9, 10), datetime(2026, 4, 10, 9, 25)),
(datetime(2026, 4, 10, 9, 15), datetime(2026, 4, 10, 9, 30)),
]
if direction == 'pad_to_kx'
else [
(datetime(2026, 4, 17, 16, 40), datetime(2026, 4, 17, 16, 55)),
(datetime(2026, 4, 17, 16, 45), datetime(2026, 4, 17, 17, 0)),
]
),
)
client = _client()
resp = client.get('/results/BRI/paris/2026-04-10?journey_type=return&return_date=2026-04-17')
resp = client.get('/results/BRI/paris/2026-04-10/return/2026-04-17')
html = resp.get_data(as_text=True)
assert resp.status_code == 200
assert 'Outbound: Bristol Temple Meads &rarr; Paris Gare du Nord' in html
assert 'Return: Paris Gare du Nord &rarr; Bristol Temple Meads' in html
assert '/results/BRI/paris/2026-04-09/return/2026-04-17' in html
assert '/results/BRI/paris/2026-04-11/return/2026-04-17' in html
assert "/results/BRI/paris/2026-04-10/return/2026-04-17" in html
assert 'journey_type=return' not in html
assert 'return_date=2026-04-17' not in html
assert 'Circle 09:10 &rarr; KX 09:25' in html
assert 'next 09:15 &rarr; KX 09:30' in html
assert 'Circle 16:40 &rarr; PAD 16:55' in html
assert 'next 16:45 &rarr; PAD 17:00' in html
assert 'title="Tight connection">⚠️</span>' in html
assert 'ES 9014' in html
assert 'ES 9035' in html

View file

@ -151,23 +151,29 @@ def _stub_single_data(monkeypatch):
},
},
)
advance_fares = {
"07:00": {
"advance_std": {
"ticket": "Advance Single",
"price": 50.0,
"code": "ADV",
},
"advance_1st": {
"ticket": "1st Advance",
"price": 80.0,
"code": "AFA",
},
},
}
monkeypatch.setattr(
app_module.gwr_fares_scraper,
"fetch_advance",
lambda station_crs, travel_date: {
"07:00": {
"advance_std": {
"ticket": "Advance Single",
"price": 50.0,
"code": "ADV",
},
"advance_1st": {
"ticket": "1st Advance",
"price": 80.0,
"code": "AFA",
},
},
},
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,
@ -269,19 +275,25 @@ def test_single_next_date_advance_standard_labels_unreachable_rows(monkeypatch):
},
},
)
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: {
"07:00": {
"advance_std": {
"ticket": "Advance Single",
"price": 50.0,
"code": "ADV",
},
"advance_1st": None,
},
},
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,
@ -386,12 +398,13 @@ def test_return_advance_first_standard_premier_totals(local_server):
timeout=10000,
)
assert "journey_type=return" in page.url
assert "return_date=2026-07-27" in page.url
assert "/results/BRI/paris/2026-07-20/return/2026-07-27" in page.url
assert "journey_type=return" not in page.url
assert "return_date=2026-07-27" not in page.url
assert "nr_class=advance_1st" in page.url
assert "es_class=plus" in page.url
totals = [el.inner_text() for el in page.locator(".total-price").all()]
assert totals == ["£172.10 high", "£127.10 low"]
assert totals == ["£172.10 💸", "£127.10 🪙"]
browser.close()
@ -400,9 +413,8 @@ def test_return_advance_first_standard_premier_totals_on_initial_url(local_serve
browser = _launch_browser(p)
page = browser.new_page()
page.goto(
f"{local_server}/results/BRI/paris/2026-07-20"
"?journey_type=return&return_date=2026-07-27"
"&nr_class=advance_1st&es_class=plus",
f"{local_server}/results/BRI/paris/2026-07-20/return/2026-07-27"
"?nr_class=advance_1st&es_class=plus",
wait_until="domcontentloaded",
)
@ -418,5 +430,5 @@ def test_return_advance_first_standard_premier_totals_on_initial_url(local_serve
)
totals = [el.inner_text() for el in page.locator(".total-price").all()]
assert totals == ["£172.10 high", "£127.10 low"]
assert totals == ["£172.10 💸", "£127.10 🪙"]
browser.close()