diff --git a/tests/Feature/Livewire/FuelFinderTest.php b/tests/Feature/Livewire/FuelFinderTest.php new file mode 100644 index 0000000..42754fe --- /dev/null +++ b/tests/Feature/Livewire/FuelFinderTest.php @@ -0,0 +1,223 @@ +assertStatus(200) + ->assertSeeHtml('name="search"'); +}); + +it('has default property values', function () { + Livewire::test(FuelFinder::class) + ->assertSet('search', '') + ->assertSet('fuelType', 'petrol') + ->assertSet('radius', 5) + ->assertSet('sort', 'reliable') + ->assertSet('results', []) + ->assertSet('meta', []) + ->assertSet('prediction', null) + ->assertSet('apiError', null) + ->assertSet('hasSearched', false); +}); + +it('validates search is required', function () { + Livewire::test(FuelFinder::class) + ->call('findStations') + ->assertHasErrors(['search' => 'required']); +}); + +it('validates fuelType is required', function () { + Livewire::test(FuelFinder::class) + ->set('search', 'SW1A 1AA') + ->set('fuelType', '') + ->call('findStations') + ->assertHasErrors(['fuelType' => 'required']); +}); + +it('populates results, meta, and prediction on successful search', function () { + Http::fake([ + '*/api/stations*' => Http::response([ + 'data' => [ + [ + 'station_id' => 'abc123', + 'name' => 'BP Garage', + 'brand' => 'BP', + 'is_supermarket' => false, + 'address' => '1 High Street', + 'postcode' => 'SW1A 1AA', + 'lat' => 51.5074, + 'lng' => -0.1278, + 'distance_km' => 1.5, + 'fuel_type' => 'e10', + 'price_pence' => 14390, + 'price' => 143.9, + 'price_updated_at' => '2026-04-05T08:00:00.000Z', + 'price_classification' => 'current', + 'price_classification_label' => 'Current', + ], + ], + 'meta' => [ + 'count' => 1, + 'lowest_pence' => 14390, + 'avg_pence' => 14390.0, + ], + ], 200), + '*/api/prediction*' => Http::response([ + 'action' => 'fill_now', + 'confidence_score' => 80.0, + 'confidence_label' => 'high', + 'reasoning' => 'Prices rising.', + 'predicted_direction' => 'up', + 'predicted_change_pence' => 3.5, + ], 200), + ]); + + Livewire::test(FuelFinder::class) + ->set('search', 'SW1A 1AA') + ->set('fuelType', 'petrol') + ->call('findStations') + ->assertSet('hasSearched', true) + ->assertSet('apiError', null) + ->assertSet('results', fn (array $r) => count($r) === 1 && $r[0]['name'] === 'BP Garage') + ->assertSet('meta', fn (array $m) => $m['count'] === 1) + ->assertSet('prediction', fn (?array $p) => $p !== null && $p['action'] === 'fill_now'); +}); + +it('sets apiError from 422 station response and leaves prediction null', function () { + Http::fake([ + '*/api/stations*' => Http::response([ + 'errors' => ['postcode' => ['Postcode not found.']], + ], 422), + ]); + + Livewire::test(FuelFinder::class) + ->set('search', 'ZZ99 9ZZ') + ->set('fuelType', 'petrol') + ->call('findStations') + ->assertSet('results', []) + ->assertSet('meta', []) + ->assertSet('prediction', null) + ->assertSet('hasSearched', false) + ->assertSet('apiError', 'Postcode not found.'); +}); + +it('sets generic apiError on server error', function () { + Http::fake([ + '*/api/stations*' => Http::response([], 500), + ]); + + Livewire::test(FuelFinder::class) + ->set('search', 'SW1A 1AA') + ->set('fuelType', 'petrol') + ->call('findStations') + ->assertSet('apiError', 'Unable to fetch stations. Please try again.'); +}); + +it('converts radius from miles to km in the outgoing stations request', function () { + Http::fake([ + '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200), + '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200), + ]); + + Livewire::test(FuelFinder::class) + ->set('search', 'SW1A 1AA') + ->set('fuelType', 'petrol') + ->set('radius', 5) + ->call('findStations'); + + Http::assertSent(function ($request) { + if (! str_contains($request->url(), 'api/stations')) { + return false; + } + $data = $request->data(); + + return isset($data['radius']) && abs((float) $data['radius'] - 8.05) < 0.01; + }); +}); + +it('resets state before each new search', function () { + Http::fake([ + '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200), + '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200), + ]); + + Livewire::test(FuelFinder::class) + ->set('search', 'SW1A 1AA') + ->set('fuelType', 'petrol') + ->set('results', [['name' => 'Old Result']]) + ->set('apiError', 'Old error') + ->call('findStations') + ->assertSet('apiError', null) + ->assertSet('results', []); +}); + +it('does not call findStations on updatedFuelType if not yet searched', function () { + Http::fake(); + + Livewire::test(FuelFinder::class) + ->set('fuelType', 'diesel'); + + Http::assertNothingSent(); +}); + +it('re-runs findStations on updatedFuelType when already searched', function () { + Http::fake([ + '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200), + '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200), + ]); + + Livewire::test(FuelFinder::class) + ->set('hasSearched', true) + ->set('search', 'SW1A 1AA') + ->set('fuelType', 'diesel'); + + Http::assertSentCount(2); // stations + prediction +}); + +it('re-runs findStations on updatedRadius when already searched', function () { + Http::fake([ + '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200), + '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200), + ]); + + Livewire::test(FuelFinder::class) + ->set('hasSearched', true) + ->set('search', 'SW1A 1AA') + ->set('radius', 10); + + Http::assertSentCount(2); +}); + +it('re-runs findStations on updatedSort when already searched', function () { + Http::fake([ + '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200), + '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200), + ]); + + Livewire::test(FuelFinder::class) + ->set('hasSearched', true) + ->set('search', 'SW1A 1AA') + ->set('sort', 'price'); + + Http::assertSentCount(2); +}); + +it('prediction remains null when prediction api fails', function () { + Http::fake([ + '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200), + '*/api/prediction*' => Http::response([], 500), + ]); + + Livewire::test(FuelFinder::class) + ->set('search', 'SW1A 1AA') + ->set('fuelType', 'petrol') + ->call('findStations') + ->assertSet('hasSearched', true) + ->assertSet('prediction', null); +});