create(array_merge([ 'name' => 'Studio News', 'slug' => 'studio-news', 'description' => 'Studio News category', 'position' => 0, 'is_active' => true, ], $attributes)); } function studioNewsTag(array $attributes = []): NewsTag { return NewsTag::query()->create(array_merge([ 'name' => 'Studio', 'slug' => 'studio', ], $attributes)); } it('forbids newsroom studio pages for non moderators', function (): void { $user = User::factory()->create(); $this->actingAs($user) ->get(route('studio.news.index')) ->assertForbidden(); }); it('renders newsroom studio pages for moderators', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'modnews', 'name' => 'Moderator News', ]); $author = User::factory()->create([ 'username' => 'writernews', 'name' => 'Writer News', ]); $category = studioNewsCategory(); $tag = studioNewsTag(); $article = NewsArticle::query()->create([ 'title' => 'Moderated newsroom article', 'slug' => 'moderated-newsroom-article', 'excerpt' => 'Studio-managed newsroom article.', 'content' => 'Studio body', 'author_id' => $author->id, 'category_id' => $category->id, 'type' => NewsArticle::TYPE_EDITORIAL, 'status' => 'published', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_PUBLISHED, 'published_at' => Carbon::parse('2026-04-05 09:30:00'), ]); $article->tags()->sync([$tag->id]); $this->actingAs($moderator) ->get(route('studio.news.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioNewsIndex') ->where('title', 'Newsroom') ->where('listing.items.0.title', 'Moderated newsroom article') ->where('createUrl', route('studio.news.create'))); $this->actingAs($moderator) ->get(route('studio.news.create')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioNewsEditor') ->where('title', 'Create article') ->has('typeOptions') ->has('statusOptions') ->has('categoryOptions') ->has('tagOptions') ->where('defaultAuthor.id', $moderator->id)); $this->actingAs($moderator) ->get(route('studio.news.categories')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioNewsTaxonomies') ->where('activeTab', 'categories') ->has('categories.0') ->has('tags.0')); $this->actingAs($moderator) ->get(route('studio.news.preview', ['article' => $article->id])) ->assertOk() ->assertSee('Preview mode') ->assertSee('Moderated newsroom article'); }); it('filters newsroom listing by status type and category', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', ]); $category = studioNewsCategory([ 'name' => 'Filtered Category', 'slug' => 'filtered-category', ]); $otherCategory = studioNewsCategory([ 'name' => 'Other Category', 'slug' => 'other-category', ]); $author = User::factory()->create(); NewsArticle::query()->create([ 'title' => 'Keep Me', 'slug' => 'keep-me', 'excerpt' => 'Should survive filtering.', 'content' => 'Content', 'author_id' => $author->id, 'category_id' => $category->id, 'type' => NewsArticle::TYPE_ANNOUNCEMENT, 'status' => 'draft', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, ]); NewsArticle::query()->create([ 'title' => 'Drop Me By Type', 'slug' => 'drop-me-type', 'excerpt' => 'Wrong type.', 'content' => 'Content', 'author_id' => $author->id, 'category_id' => $category->id, 'type' => NewsArticle::TYPE_EDITORIAL, 'status' => 'draft', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, ]); NewsArticle::query()->create([ 'title' => 'Drop Me By Category', 'slug' => 'drop-me-category', 'excerpt' => 'Wrong category.', 'content' => 'Content', 'author_id' => $author->id, 'category_id' => $otherCategory->id, 'type' => NewsArticle::TYPE_ANNOUNCEMENT, 'status' => 'draft', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, ]); $this->actingAs($moderator) ->get(route('studio.news.index', [ 'status' => NewsArticle::EDITORIAL_STATUS_DRAFT, 'type' => NewsArticle::TYPE_ANNOUNCEMENT, 'category_id' => $category->id, ])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioNewsIndex') ->where('listing.filters.status', NewsArticle::EDITORIAL_STATUS_DRAFT) ->where('listing.filters.type', NewsArticle::TYPE_ANNOUNCEMENT) ->where('listing.filters.category_id', $category->id) ->has('listing.items', 1) ->where('listing.items.0.title', 'Keep Me')); }); it('stores a newsroom draft with taxonomy links', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'editornews', 'name' => 'Editor News', ]); $author = User::factory()->create(); $category = studioNewsCategory([ 'name' => 'Launches', 'slug' => 'launches', ]); $tag = studioNewsTag([ 'name' => 'Update', 'slug' => 'update', ]); $response = $this->actingAs($moderator)->post(route('studio.news.store'), [ 'title' => 'Stored newsroom draft', 'slug' => 'stored-newsroom-draft', 'excerpt' => 'Stored through the Studio newsroom form.', 'content' => 'This article was created through the new Studio News flow.', 'type' => NewsArticle::TYPE_ANNOUNCEMENT, 'category_id' => $category->id, 'author_id' => $author->id, 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, 'published_at' => null, 'tag_ids' => [$tag->id], 'new_tag_names' => ['Studio Exclusive'], 'is_featured' => true, 'is_pinned' => false, 'meta_title' => 'Stored newsroom draft meta', 'meta_description' => 'Stored newsroom draft description', ]); $article = NewsArticle::query()->where('slug', 'stored-newsroom-draft')->firstOrFail(); $response->assertRedirect(route('studio.news.edit', ['article' => $article->id])); $this->assertDatabaseHas('news_articles', [ 'id' => $article->id, 'title' => 'Stored newsroom draft', 'slug' => 'stored-newsroom-draft', 'author_id' => $author->id, 'category_id' => $category->id, 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, 'status' => 'draft', ]); expect($article->tags()->pluck('news_tags.name')->all()) ->toContain('Update') ->toContain('Studio Exclusive'); }); it('updates an existing newsroom article', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', ]); $author = User::factory()->create(); $category = studioNewsCategory([ 'name' => 'Editorial', 'slug' => 'editorial', ]); $tag = studioNewsTag([ 'name' => 'Feature', 'slug' => 'feature', ]); $article = NewsArticle::query()->create([ 'title' => 'Original newsroom article', 'slug' => 'original-newsroom-article', 'excerpt' => 'Original excerpt.', 'content' => 'Original content.', 'author_id' => $author->id, 'type' => NewsArticle::TYPE_ANNOUNCEMENT, 'status' => 'draft', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, ]); $this->actingAs($moderator) ->patch(route('studio.news.update', ['article' => $article->id]), [ 'title' => 'Updated newsroom article', 'slug' => 'updated-newsroom-article', 'excerpt' => 'Updated excerpt.', 'content' => '

Updated content.

', 'type' => NewsArticle::TYPE_EDITORIAL, 'category_id' => $category->id, 'author_id' => $author->id, 'editorial_status' => NewsArticle::EDITORIAL_STATUS_IN_REVIEW, 'tag_ids' => [$tag->id], 'new_tag_names' => ['Deep Dive'], 'is_featured' => true, 'is_pinned' => true, ]) ->assertSessionHasNoErrors() ->assertRedirect(); $article->refresh(); expect($article->title)->toBe('Updated newsroom article') ->and($article->slug)->toBe('updated-newsroom-article') ->and($article->type)->toBe(NewsArticle::TYPE_EDITORIAL) ->and($article->editorial_status)->toBe(NewsArticle::EDITORIAL_STATUS_IN_REVIEW) ->and((int) $article->category_id)->toBe($category->id) ->and((bool) $article->is_featured)->toBeTrue() ->and((bool) $article->is_pinned)->toBeTrue() ->and($article->tags()->pluck('news_tags.name')->all())->toContain('Feature') ->and($article->tags()->pluck('news_tags.name')->all())->toContain('Deep Dive'); }); it('soft deletes a newsroom article from studio', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', ]); $author = User::factory()->create(); $article = NewsArticle::query()->create([ 'title' => 'Delete me softly', 'slug' => 'delete-me-softly', 'excerpt' => 'Soft delete test article.', 'content' => 'Studio delete content.', 'author_id' => $author->id, 'type' => NewsArticle::TYPE_EDITORIAL, 'status' => 'draft', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_DRAFT, ]); $this->actingAs($moderator) ->delete(route('studio.news.destroy', ['article' => $article->id])) ->assertRedirect(route('studio.news.index')); $this->assertSoftDeleted('news_articles', [ 'id' => $article->id, ]); });