admin = User::factory()->admin()->create(); }); // =========================================== // Index Page Tests // =========================================== test('admin can view posts index', function () { $this->actingAs($this->admin) ->get(route('admin.posts.index')) ->assertOk(); }); test('admin can see list of posts', function () { $post = Post::factory()->create([ 'title' => ['ar' => 'عنوان عربي', 'en' => 'English Title'], ]); $this->actingAs($this->admin); // Use Volt test to check posts are in the view data (locale independent) $component = Volt::test('admin.posts.index'); expect($component->viewData('posts')->total())->toBe(1); expect($component->viewData('posts')->first()->id)->toBe($post->id); }); test('posts list shows status badges', function () { Post::factory()->draft()->create(); Post::factory()->published()->create(); $this->actingAs($this->admin); $component = Volt::test('admin.posts.index'); expect($component->viewData('posts')->total())->toBe(2); }); test('admin can filter posts by status', function () { Post::factory()->draft()->create(); Post::factory()->published()->create(); $this->actingAs($this->admin); $component = Volt::test('admin.posts.index') ->set('statusFilter', 'draft'); expect($component->viewData('posts')->total())->toBe(1); expect($component->viewData('posts')->first()->status)->toBe(PostStatus::Draft); }); test('admin can search posts by title', function () { Post::factory()->create(['title' => ['ar' => 'عنوان أول', 'en' => 'First Post']]); Post::factory()->create(['title' => ['ar' => 'عنوان ثاني', 'en' => 'Second Post']]); $this->actingAs($this->admin); $component = Volt::test('admin.posts.index') ->set('search', 'First'); expect($component->viewData('posts')->total())->toBe(1); }); test('admin can search posts by body content', function () { Post::factory()->create([ 'title' => ['ar' => 'عنوان أول', 'en' => 'First Post'], 'body' => ['ar' => 'محتوى فريد', 'en' => 'unique content here'], ]); Post::factory()->create([ 'title' => ['ar' => 'عنوان ثاني', 'en' => 'Second Post'], 'body' => ['ar' => 'محتوى آخر', 'en' => 'different content'], ]); $this->actingAs($this->admin); $component = Volt::test('admin.posts.index') ->set('search', 'unique'); expect($component->viewData('posts')->total())->toBe(1); }); test('admin can sort posts by created date', function () { $older = Post::factory()->create(['created_at' => now()->subDays(5)]); $newer = Post::factory()->create(['created_at' => now()]); $this->actingAs($this->admin); // Sort by created_at ascending (oldest first) $component = Volt::test('admin.posts.index') ->call('sort', 'created_at'); expect($component->viewData('posts')->first()->id)->toBe($older->id); // Sort again (descending - newest first) $component->call('sort', 'created_at'); expect($component->viewData('posts')->first()->id)->toBe($newer->id); }); test('admin can sort posts by updated date', function () { $older = Post::factory()->create(['updated_at' => now()->subDays(5)]); $newer = Post::factory()->create(['updated_at' => now()]); $this->actingAs($this->admin); // Default sort is updated_at desc (newest first) $component = Volt::test('admin.posts.index'); expect($component->viewData('posts')->first()->id)->toBe($newer->id); }); test('admin can toggle post publish status from draft to published', function () { $post = Post::factory()->draft()->create(); $this->actingAs($this->admin); Volt::test('admin.posts.index') ->call('togglePublish', $post->id); expect($post->fresh()->status)->toBe(PostStatus::Published); expect($post->fresh()->published_at)->not->toBeNull(); }); test('admin can toggle post publish status from published to draft', function () { $post = Post::factory()->published()->create(); $this->actingAs($this->admin); Volt::test('admin.posts.index') ->call('togglePublish', $post->id); expect($post->fresh()->status)->toBe(PostStatus::Draft); expect($post->fresh()->published_at)->toBeNull(); }); test('toggle publish creates audit log', function () { $post = Post::factory()->draft()->create(); $this->actingAs($this->admin); Volt::test('admin.posts.index') ->call('togglePublish', $post->id); expect(AdminLog::where('action', 'status_change') ->where('target_type', 'post') ->where('target_id', $post->id) ->exists())->toBeTrue(); $log = AdminLog::where('target_id', $post->id)->where('action', 'status_change')->first(); expect($log->old_values['status'])->toBe('draft'); expect($log->new_values['status'])->toBe('published'); }); test('admin can delete post from index', function () { $post = Post::factory()->create(); $postId = $post->id; $this->actingAs($this->admin); Volt::test('admin.posts.index') ->call('delete', $post->id); expect(Post::find($postId))->toBeNull(); }); test('delete post creates audit log', function () { $post = Post::factory()->create(); $postId = $post->id; $this->actingAs($this->admin); Volt::test('admin.posts.index') ->call('delete', $post->id); expect(AdminLog::where('action', 'delete') ->where('target_type', 'post') ->where('target_id', $postId) ->exists())->toBeTrue(); }); test('pagination works correctly with per page selector', function () { Post::factory()->count(15)->create(); $this->actingAs($this->admin); // Default is 10 per page $component = Volt::test('admin.posts.index'); expect($component->viewData('posts')->perPage())->toBe(10); expect($component->viewData('posts')->total())->toBe(15); // Change to 25 per page $component->set('perPage', 25); expect($component->viewData('posts')->perPage())->toBe(25); // Change to 50 per page $component->set('perPage', 50); expect($component->viewData('posts')->perPage())->toBe(50); }); test('toggle publish handles post not found gracefully', function () { $this->actingAs($this->admin); // Should not throw exception when post doesn't exist Volt::test('admin.posts.index') ->call('togglePublish', 99999) ->assertOk(); }); test('delete handles post not found gracefully', function () { $this->actingAs($this->admin); // Should not throw exception when post doesn't exist Volt::test('admin.posts.index') ->call('delete', 99999) ->assertOk(); }); // =========================================== // Create Page Tests // =========================================== test('admin can view post creation form', function () { $this->actingAs($this->admin) ->get(route('admin.posts.create')) ->assertOk(); }); test('admin can create post with valid bilingual content', function () { $this->actingAs($this->admin); Volt::test('admin.posts.create') ->set('title_ar', 'عنوان المقال') ->set('title_en', 'Article Title') ->set('body_ar', '

محتوى المقال

') ->set('body_en', '

Article content

') ->call('saveDraft') ->assertHasNoErrors(); expect(Post::where('title->en', 'Article Title')->exists())->toBeTrue(); }); test('create post fails with missing required fields', function () { $this->actingAs($this->admin); Volt::test('admin.posts.create') ->set('title_ar', '') ->set('title_en', 'Title') ->set('body_ar', 'content') ->set('body_en', 'content') ->call('save') ->assertHasErrors(['title_ar']); }); test('save draft preserves draft status', function () { $this->actingAs($this->admin); Volt::test('admin.posts.create') ->set('title_ar', 'عنوان') ->set('title_en', 'Title') ->set('body_ar', 'محتوى') ->set('body_en', 'Content') ->call('saveDraft'); expect(Post::first()->status)->toBe(PostStatus::Draft); }); test('publish changes status to published', function () { $this->actingAs($this->admin); Volt::test('admin.posts.create') ->set('title_ar', 'عنوان') ->set('title_en', 'Title') ->set('body_ar', 'محتوى') ->set('body_en', 'Content') ->call('publish'); expect(Post::first()->status)->toBe(PostStatus::Published); expect(Post::first()->published_at)->not->toBeNull(); }); test('HTML sanitization removes script tags but keeps allowed formatting', function () { $this->actingAs($this->admin); Volt::test('admin.posts.create') ->set('title_ar', 'عنوان') ->set('title_en', 'Title') ->set('body_ar', '

نص

') ->set('body_en', '

Safe

Bold') ->call('saveDraft'); $post = Post::first(); expect($post->body['en'])->not->toContain('