diff --git a/app/Livewire/Feeds.php b/app/Livewire/Feeds.php index b1f86be7..1ac63001 100644 --- a/app/Livewire/Feeds.php +++ b/app/Livewire/Feeds.php @@ -2,12 +2,25 @@ namespace App\Livewire; +use App\Actions\CreateFeedAction; use App\Models\Feed; +use App\Models\Language; use Illuminate\Contracts\View\View; +use InvalidArgumentException; use Livewire\Component; class Feeds extends Component { + public bool $showCreateModal = false; + + public string $newName = ''; + + public string $newProvider = ''; + + public ?int $newLanguageId = null; + + public string $newDescription = ''; + public function toggle(int $feedId): void { $feed = Feed::findOrFail($feedId); @@ -15,12 +28,64 @@ public function toggle(int $feedId): void $feed->save(); } + public function openCreateModal(): void + { + $this->reset(['newName', 'newProvider', 'newLanguageId', 'newDescription']); + $this->resetErrorBag(); + $this->showCreateModal = true; + } + + public function closeCreateModal(): void + { + $this->showCreateModal = false; + } + + public function createFeed(CreateFeedAction $action): void + { + $providers = array_keys($this->activeProviders()); + + $this->validate([ + 'newName' => 'required|string|max:255', + 'newProvider' => ['required', 'string', 'in:'.implode(',', $providers)], + 'newLanguageId' => 'required|integer|exists:languages,id', + ]); + + try { + $action->execute( + $this->newName, + $this->newProvider, + $this->newLanguageId, + // Blade textarea binds an empty string when blank; the action expects null for "no description". + $this->newDescription !== '' ? $this->newDescription : null, + ); + } catch (InvalidArgumentException $e) { + $this->addError('newProvider', 'This provider is not available for the selected language.'); + + return; + } + + $this->closeCreateModal(); + } + + /** + * @return array> + */ + private function activeProviders(): array + { + /** @var array> $providers */ + $providers = config('feed.providers', []); + + return array_filter($providers, fn (array $provider): bool => ($provider['is_active'] ?? false) === true); + } + public function render(): View { $feeds = Feed::orderBy('name')->get(); return view('livewire.feeds', [ 'feeds' => $feeds, + 'providers' => $this->activeProviders(), + 'languages' => Language::where('is_active', true)->orderBy('name')->get(), ])->layout('layouts.app'); } } diff --git a/resources/views/livewire/feeds.blade.php b/resources/views/livewire/feeds.blade.php index 5f4fc72f..7ce870fc 100644 --- a/resources/views/livewire/feeds.blade.php +++ b/resources/views/livewire/feeds.blade.php @@ -1,5 +1,15 @@
- + + +
@forelse ($feeds as $feed) @@ -76,7 +86,112 @@ class="text-gray-400 hover:text-gray-600"

No feeds have been configured yet.

+
+ +
@endforelse
+ + + @if ($showCreateModal) + + @endif diff --git a/tests/Feature/Livewire/FeedsTest.php b/tests/Feature/Livewire/FeedsTest.php new file mode 100644 index 00000000..5e518173 --- /dev/null +++ b/tests/Feature/Livewire/FeedsTest.php @@ -0,0 +1,157 @@ +create(['name' => 'Existing Feed']); + + Livewire::test(Feeds::class) + ->assertSee('Add Feed') + ->assertSee('Existing Feed'); + } + + public function test_add_button_renders_in_empty_state(): void + { + Livewire::test(Feeds::class) + ->assertSee('No feeds') + ->assertSee('Add Feed'); + } + + public function test_open_create_modal_shows_modal(): void + { + Livewire::test(Feeds::class) + ->assertSet('showCreateModal', false) + ->call('openCreateModal') + ->assertSet('showCreateModal', true); + } + + public function test_create_feed_requires_name(): void + { + $language = Language::factory()->english()->create(); + + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newProvider', 'guardian') + ->set('newLanguageId', $language->id) + ->call('createFeed') + ->assertHasErrors(['newName' => 'required']); + } + + public function test_create_feed_requires_provider(): void + { + $language = Language::factory()->english()->create(); + + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newName', 'Tech News') + ->set('newLanguageId', $language->id) + ->call('createFeed') + ->assertHasErrors(['newProvider' => 'required']); + } + + public function test_create_feed_requires_language(): void + { + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newName', 'Tech News') + ->set('newProvider', 'guardian') + ->call('createFeed') + ->assertHasErrors(['newLanguageId' => 'required']); + } + + public function test_create_feed_rejects_unknown_provider(): void + { + $language = Language::factory()->english()->create(); + + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newName', 'Tech News') + ->set('newProvider', 'nonexistent') + ->set('newLanguageId', $language->id) + ->call('createFeed') + ->assertHasErrors('newProvider'); + + $this->assertDatabaseCount('feeds', 0); + } + + public function test_create_feed_succeeds_and_resolves_url(): void + { + $language = Language::factory()->english()->create(); + + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newName', 'Guardian News') + ->set('newProvider', 'guardian') + ->set('newLanguageId', $language->id) + ->set('newDescription', 'British daily') + ->call('createFeed') + ->assertHasNoErrors() + ->assertSet('showCreateModal', false); + + $this->assertDatabaseHas('feeds', [ + 'name' => 'Guardian News', + 'provider' => 'guardian', + 'language_id' => $language->id, + 'url' => 'https://www.theguardian.com/international/rss', + 'description' => 'British daily', + 'is_active' => true, + ]); + } + + public function test_create_feed_leaves_description_null_when_blank(): void + { + $language = Language::factory()->english()->create(); + + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newName', 'Guardian News') + ->set('newProvider', 'guardian') + ->set('newLanguageId', $language->id) + ->call('createFeed') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('feeds', [ + 'name' => 'Guardian News', + 'description' => null, + ]); + } + + public function test_create_feed_surfaces_invalid_provider_language_combination(): void + { + // Belga only supports 'en'; a non-'en' language triggers InvalidArgumentException in the action. + $language = Language::factory()->create(['short_code' => 'nl', 'name' => 'Dutch', 'is_active' => true]); + + Livewire::test(Feeds::class) + ->call('openCreateModal') + ->set('newName', 'Belga NL') + ->set('newProvider', 'belga') + ->set('newLanguageId', $language->id) + ->call('createFeed') + ->assertHasErrors('newProvider') + ->assertSet('showCreateModal', true); + + $this->assertDatabaseCount('feeds', 0); + } + + public function test_toggle_flips_active_state(): void + { + $feed = Feed::factory()->create(['is_active' => true]); + + Livewire::test(Feeds::class) + ->call('toggle', $feed->id); + + $this->assertFalse($feed->fresh()->is_active); + } +}