Replace the Eurostar timetable link with the search URL (eurostar.com/search/uk-en?adult=1&origin=…&destination=…&outbound=…) so the footer links directly to the page that shows prices for the specific date and destination. Add a Bristol Temple Meads → Paddington departures link on RTT alongside the existing Paddington arrivals link. Also update "morning service unavailable" badge and tests to reflect the removal of the morning-only cutoff filter from find_unreachable_morning_eurostars. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
271 lines
9.9 KiB
Python
271 lines
9.9 KiB
Python
import app as app_module
|
||
|
||
|
||
def _client():
|
||
app_module.app.config['TESTING'] = True
|
||
return app_module.app.test_client()
|
||
|
||
|
||
def _stub_data(monkeypatch, prices=None):
|
||
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: [
|
||
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'fetch',
|
||
lambda destination, travel_date, user_agent: [
|
||
{
|
||
'depart_st_pancras': '10:01',
|
||
'arrive_destination': '13:34',
|
||
'destination': destination,
|
||
'train_number': 'ES 9014',
|
||
},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'timetable_url',
|
||
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
|
||
)
|
||
_prices = prices if prices is not None else {}
|
||
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: _prices)
|
||
|
||
|
||
def test_index_shows_fixed_departure_and_destination_radios():
|
||
client = _client()
|
||
|
||
resp = client.get('/')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert 'Departure point' in html
|
||
assert 'Bristol Temple Meads' in html
|
||
assert html.count('type="radio"') == len(app_module.DESTINATIONS)
|
||
assert 'destination-rotterdam' in html
|
||
|
||
|
||
def test_search_redirects_to_results_with_selected_params():
|
||
client = _client()
|
||
|
||
resp = client.get('/search?destination=rotterdam&travel_date=2026-04-10&min_connection=60&max_connection=120')
|
||
|
||
assert resp.status_code == 302
|
||
assert resp.headers['Location'].endswith(
|
||
'/results/rotterdam/2026-04-10?min_connection=60&max_connection=120'
|
||
)
|
||
|
||
|
||
def test_results_shows_same_day_destination_switcher(monkeypatch):
|
||
_stub_data(monkeypatch)
|
||
client = _client()
|
||
|
||
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert 'Switch destination for Friday 10 April 2026' in html
|
||
assert '<span class="chip-current">Paris Gare du Nord</span>' in html
|
||
assert '/results/brussels/2026-04-10?min_connection=60&max_connection=120' in html
|
||
assert '/results/rotterdam/2026-04-10?min_connection=60&max_connection=120' in html
|
||
assert 'ES 9014' in html
|
||
|
||
|
||
def test_results_title_and_social_meta_include_destination(monkeypatch):
|
||
_stub_data(monkeypatch)
|
||
client = _client()
|
||
|
||
resp = client.get('/results/lille/2026-04-10?min_connection=60&max_connection=120')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert '<title>Bristol to Lille Europe via Eurostar</title>' in html
|
||
assert '<meta property="og:title" content="Bristol to Lille Europe via Eurostar">' in html
|
||
assert (
|
||
'<meta property="og:description" content="Train options from Bristol Temple Meads '
|
||
'to Lille Europe on Friday 10 April 2026 via Paddington, St Pancras, and Eurostar.">'
|
||
) in html
|
||
assert '<meta property="og:url" content="http://localhost/results/lille/2026-04-10?min_connection=60&max_connection=120">' in html
|
||
|
||
|
||
def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(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, 'fetch_eurostar_prices', lambda dest, date: {})
|
||
monkeypatch.setattr(
|
||
app_module.rtt_scraper,
|
||
'fetch',
|
||
lambda travel_date, user_agent: [
|
||
{'depart_bristol': '07:00', 'arrive_paddington': '08:30', 'headcode': '1A01'},
|
||
{'depart_bristol': '07:05', 'arrive_paddington': '08:36', 'headcode': '1A02'},
|
||
{'depart_bristol': '07:10', 'arrive_paddington': '08:46', 'headcode': '1A03'},
|
||
{'depart_bristol': '07:15', 'arrive_paddington': '08:56', 'headcode': '1A04'},
|
||
{'depart_bristol': '07:20', 'arrive_paddington': '09:06', 'headcode': '1A05'},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'fetch',
|
||
lambda destination, travel_date, user_agent: [
|
||
{
|
||
'depart_st_pancras': '09:30',
|
||
'arrive_destination': '11:50',
|
||
'destination': destination,
|
||
'train_number': 'ES 1001',
|
||
},
|
||
{
|
||
'depart_st_pancras': '09:40',
|
||
'arrive_destination': '12:00',
|
||
'destination': destination,
|
||
'train_number': 'ES 1002',
|
||
},
|
||
{
|
||
'depart_st_pancras': '09:50',
|
||
'arrive_destination': '12:20',
|
||
'destination': destination,
|
||
'train_number': 'ES 1003',
|
||
},
|
||
{
|
||
'depart_st_pancras': '10:00',
|
||
'arrive_destination': '12:35',
|
||
'destination': destination,
|
||
'train_number': 'ES 1004',
|
||
},
|
||
{
|
||
'depart_st_pancras': '10:10',
|
||
'arrive_destination': '12:45',
|
||
'destination': destination,
|
||
'train_number': 'ES 1005',
|
||
},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'timetable_url',
|
||
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
|
||
)
|
||
client = _client()
|
||
|
||
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert html.count('title="Fastest option"') == 2
|
||
assert html.count('title="Slowest option"') == 2
|
||
assert '4h 50m ⚡' in html
|
||
assert '4h 55m ⚡' in html
|
||
assert '5h 20m 🐢' in html
|
||
assert '5h 25m 🐢' in html
|
||
assert '5h 10m ⚡' not in html
|
||
assert '5h 10m 🐢' not in html
|
||
|
||
|
||
def test_results_shows_unreachable_morning_eurostar_services(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, 'fetch_eurostar_prices', lambda dest, date: {})
|
||
monkeypatch.setattr(
|
||
app_module.rtt_scraper,
|
||
'fetch',
|
||
lambda travel_date, user_agent: [
|
||
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'fetch',
|
||
lambda destination, travel_date, user_agent: [
|
||
{
|
||
'depart_st_pancras': '09:30',
|
||
'arrive_destination': '12:00',
|
||
'destination': destination,
|
||
'train_number': 'ES 9001',
|
||
},
|
||
{
|
||
'depart_st_pancras': '10:15',
|
||
'arrive_destination': '13:40',
|
||
'destination': destination,
|
||
'train_number': 'ES 9002',
|
||
},
|
||
{
|
||
'depart_st_pancras': '12:30',
|
||
'arrive_destination': '15:55',
|
||
'destination': destination,
|
||
'train_number': 'ES 9003',
|
||
},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'timetable_url',
|
||
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
|
||
)
|
||
client = _client()
|
||
|
||
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert '2 Eurostar services unavailable from Bristol' in html
|
||
assert '09:30' in html
|
||
assert 'ES 9001' in html
|
||
assert 'Unavailable from Bristol' in html
|
||
assert html.index('09:30') < html.index('10:15')
|
||
|
||
|
||
def test_results_shows_eurostar_price_and_total(monkeypatch):
|
||
# 07:00 on Friday 2026-04-10 → Anytime £138.70 (weekday, 05:05–08:25 window)
|
||
_stub_data(monkeypatch, prices={'10:01': 59})
|
||
client = _client()
|
||
|
||
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert '£59' in html # Eurostar Standard price
|
||
assert '£197.70' in html # Anytime £138.70 + ES £59
|
||
|
||
|
||
def test_results_can_show_only_unreachable_morning_services(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, 'fetch_eurostar_prices', lambda dest, date: {})
|
||
monkeypatch.setattr(
|
||
app_module.rtt_scraper,
|
||
'fetch',
|
||
lambda travel_date, user_agent: [
|
||
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'fetch',
|
||
lambda destination, travel_date, user_agent: [
|
||
{
|
||
'depart_st_pancras': '09:30',
|
||
'arrive_destination': '12:00',
|
||
'destination': destination,
|
||
'train_number': 'ES 9001',
|
||
},
|
||
],
|
||
)
|
||
monkeypatch.setattr(
|
||
app_module.eurostar_scraper,
|
||
'timetable_url',
|
||
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
|
||
)
|
||
client = _client()
|
||
|
||
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
|
||
html = resp.get_data(as_text=True)
|
||
|
||
assert resp.status_code == 200
|
||
assert 'No valid journeys found.' not in html
|
||
assert '1 Eurostar service unavailable from Bristol' in html
|
||
assert '09:30' in html
|
||
assert 'Unavailable from Bristol' in html
|