- ${this.config.train_emoji}
+ let timelineHTML = '';
+ if (this.config.show_route_details && attrs.stops_schedule) {
+ timelineHTML = `
+
-
${this.config.train_station_emoji}
-
-
- ${hasDelay && realArrivalTime ? `
-
${formattedTime}
-
${realArrivalTime}
- ` : `
-
${formattedTime}
- `}
+
`;
+ }
+
+ const timeOnly = (t) => t ? t.split(' - ')[1] : "--:--";
+
+ return `
+
+
+
+ ${pos >= 0 && pos <= 100 ? `
+
+ ${isCanceled ? 'â' : this.config.train_emoji}
+
+ ` : ''}
-
- ${hasDelay ? `+${delayMinutes}min` : 'Ă l\'heure'}
+
+
${this.config.train_station_emoji}
+
+
${hasDelay ? `${timeOnly(attrs.base_departure_time)}${timeOnly(attrs.departure_time)}` : timeOnly(attrs.departure_time)}
+
+ ${isCanceled ? 'ANNULĂ' : (hasDelay ? `+${attrs.delay_minutes}min` : 'Ă l\'heure')}
+
+ ${attrs.delay_cause ? `
${attrs.delay_cause}
` : ''}
+
-
-
- `;
-
- return html;
- }).join('');
- }
+ ${timelineHTML}
+
`;
+ }).join('');
- getCardSize() {
- return Math.max(3, this.config.train_lines + 1);
+ this.shadowRoot.innerHTML = `
+
+
+
+ ${trainLinesHTML}
+ `;
+ });
}
}
-
-// Définir l'élément custom
customElements.define('sncf-train-card', SncfTrainCard);
\ No newline at end of file
diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py
new file mode 100644
index 0000000..43578bf
--- /dev/null
+++ b/tests/test_config_flow.py
@@ -0,0 +1,90 @@
+import pytest
+from unittest.mock import AsyncMock, patch
+
+from homeassistant import config_entries
+from custom_components.sncf_trains.const import DOMAIN, CONF_API_KEY
+
+
+@pytest.mark.asyncio
+async def test_config_flow_happy_path(hass):
+ """Test config flow with valid API key and stations."""
+ mock_api = AsyncMock()
+ mock_api.search_stations = AsyncMock(
+ side_effect=[
+ [{"id": "stop_area:dep", "name": "Paris Gare de Lyon"}], # departure city
+ [{"id": "stop_area:arr", "name": "Lyon Part Dieu"}], # arrival city
+ ]
+ )
+
+ with patch(
+ "custom_components.sncf_trains.config_flow.SncfApiClient", return_value=mock_api
+ ):
+ # Step 1: saisie API key
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_API_KEY: "valid_key"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "departure_city"
+
+ # Step 2: ville départ
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"departure_city": "Paris"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "departure_station"
+
+ # Step 3: station départ
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"departure_station": "stop_area:dep"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "arrival_city"
+
+ # Step 4: ville arrivée
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"arrival_city": "Lyon"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "arrival_station"
+
+ # Step 5: station arrivée
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"arrival_station": "stop_area:arr"}
+ )
+ assert result["type"] == "form"
+ assert result["step_id"] == "time_range"
+
+ # Step 6: plage horaire + finalisation
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"time_start": "07:00", "time_end": "10:00"}
+ )
+ assert result["type"] == "create_entry"
+ assert result["title"] == "SNCF: Paris Gare de Lyon â Lyon Part Dieu"
+ assert result["data"]["departure_name"] == "Paris Gare de Lyon"
+
+
+@pytest.mark.asyncio
+async def test_config_flow_invalid_api_key(hass):
+ """Test config flow with invalid API key."""
+ mock_api = AsyncMock()
+ mock_api.search_stations = AsyncMock(return_value=None)
+
+ with patch(
+ "custom_components.sncf_trains.config_flow.SncfApiClient", return_value=mock_api
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {CONF_API_KEY: "bad_key"}
+ )
+
+ assert result["type"] == "form"
+ assert result["errors"]["base"] == "invalid_api_key"
diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py
new file mode 100644
index 0000000..1e233f1
--- /dev/null
+++ b/tests/test_coordinator.py
@@ -0,0 +1,78 @@
+import pytest
+from unittest.mock import AsyncMock
+from datetime import timedelta
+from homeassistant.helpers.update_coordinator import UpdateFailed
+
+from custom_components.sncf_trains.coordinator import SncfUpdateCoordinator
+
+
+@pytest.mark.asyncio
+async def test_coordinator_success(hass):
+ """Test coordinator fetches journeys successfully."""
+ mock_api = AsyncMock()
+ mock_api.fetch_journeys = AsyncMock(return_value=[{"id": "j1"}])
+
+ coordinator = SncfUpdateCoordinator(
+ hass=hass,
+ api_client=mock_api,
+ departure="stop_area:dep",
+ arrival="stop_area:arr",
+ time_start="06:00",
+ time_end="09:00",
+ update_interval=5,
+ outside_interval=30,
+ )
+
+ data = await coordinator._async_update_data()
+ assert data == [{"id": "j1"}]
+ mock_api.fetch_journeys.assert_called_once()
+ assert isinstance(coordinator.update_interval, timedelta)
+
+
+@pytest.mark.asyncio
+async def test_coordinator_api_failure(hass):
+ """Test coordinator raises UpdateFailed when API fails."""
+ mock_api = AsyncMock()
+ mock_api.fetch_journeys = AsyncMock(side_effect=Exception("API error"))
+
+ coordinator = SncfUpdateCoordinator(
+ hass=hass,
+ api_client=mock_api,
+ departure="stop_area:dep",
+ arrival="stop_area:arr",
+ time_start="06:00",
+ time_end="09:00",
+ )
+
+ with pytest.raises(UpdateFailed):
+ await coordinator._async_update_data()
+
+
+@pytest.mark.asyncio
+async def test_coordinator_adjust_interval(hass):
+ """Test that update interval adjusts inside and outside time range."""
+
+ mock_api = AsyncMock()
+ mock_api.fetch_journeys = AsyncMock(return_value=[{"id": "j1"}])
+
+ coordinator = SncfUpdateCoordinator(
+ hass=hass,
+ api_client=mock_api,
+ departure="stop_area:dep",
+ arrival="stop_area:arr",
+ time_start="00:00",
+ time_end="23:59",
+ update_interval=5,
+ outside_interval=30,
+ )
+
+ # Forcing inside time range (always true here)
+ await coordinator._async_update_data()
+ assert coordinator.update_interval == timedelta(minutes=5)
+
+ # Fake outside range by setting opposite times
+ coordinator.time_start = "23:59"
+ coordinator.time_end = "00:00"
+
+ await coordinator._async_update_data()
+ assert coordinator.update_interval == timedelta(minutes=30)