create(); $this->actingAs($user) ->get(route('dashboard')) ->assertOk() ->assertSee('Messages') ->assertSee('Profile settings') ->assertSee('Password and 2FA') ->assertDontSee('Repository') ->assertDontSee('Documentation'); }); test('conversation list only shows conversations the user participates in', function () { $user = User::factory()->create(); $teammate = User::factory()->create(['name' => 'Visible Teammate']); $outsider = User::factory()->create(['name' => 'Hidden Teammate']); $visibleConversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($visibleConversation)->for($user)->create(); ConversationParticipant::factory()->for($visibleConversation)->for($teammate)->create(); $hiddenConversation = Conversation::factory() ->group() ->for($outsider, 'creator') ->create(['name' => 'Hidden Group']); ConversationParticipant::factory()->for($hiddenConversation)->for($outsider)->create(); $this->actingAs($user); Livewire::test(ConversationList::class) ->assertSee('Visible Teammate') ->assertSee('Opening') ->assertDontSee('Hidden Group'); }); test('users can create direct conversations', function () { $user = User::factory()->create(); $teammate = User::factory()->create(['name' => 'Direct Partner']); $this->actingAs($user); Livewire::test(ConversationList::class) ->call('openCreateConversation') ->call('toggleMember', $teammate->id) ->call('createConversation') ->assertHasNoErrors(); $conversation = Conversation::query() ->where('type', Conversation::TypeDirect) ->whereHas('participants', fn ($query) => $query->where('user_id', $user->id)) ->whereHas('participants', fn ($query) => $query->where('user_id', $teammate->id)) ->first(); expect($conversation)->not->toBeNull(); expect($conversation->participants()->count())->toBe(2); }); test('users can create group conversations with members', function () { $user = User::factory()->create(); $firstMember = User::factory()->create(['name' => 'Launch Lead']); $secondMember = User::factory()->create(['name' => 'Design Lead']); $this->actingAs($user); Livewire::test(ConversationList::class) ->set('createType', Conversation::TypeGroup) ->set('groupName', 'Launch Room') ->call('toggleMember', $firstMember->id) ->call('toggleMember', $secondMember->id) ->call('createConversation') ->assertHasNoErrors(); $conversation = Conversation::query() ->where('type', Conversation::TypeGroup) ->where('name', 'Launch Room') ->first(); expect($conversation)->not->toBeNull(); expect($conversation->participants()->count())->toBe(3); }); test('participants can send messages', function () { $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); $this->actingAs($user); Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id]) ->set('body', 'This looks ready to ship.') ->call('sendMessage') ->assertSet('body', ''); expect(Message::query() ->where('conversation_id', $conversation->id) ->where('user_id', $user->id) ->where('body', 'This looks ready to ship.') ->exists())->toBeTrue(); }); test('participants can add emojis from the composer picker', function () { $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); $emoji = html_entity_decode('👍', ENT_QUOTES, 'UTF-8'); $this->actingAs($user); Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id]) ->call('toggleEmojiPicker') ->assertSet('emojiPickerOpen', true) ->assertSee('Add Thumbs up emoji') ->set('body', 'Looks good') ->call('appendEmoji', '1F44D') ->assertSet('body', 'Looks good '.$emoji) ->assertSet('emojiPickerOpen', false); }); test('participants can send file messages', function () { Storage::fake('local'); $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); $this->actingAs($user); Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id]) ->set('attachments', [ UploadedFile::fake()->create('handoff.pdf', 64, 'application/pdf'), ]) ->call('sendMessage') ->assertHasNoErrors() ->assertSet('body', '') ->assertSet('attachments', []); $message = Message::query() ->where('conversation_id', $conversation->id) ->where('user_id', $user->id) ->where('type', Message::TypeFile) ->first(); expect($message)->not->toBeNull(); expect($message->attachmentName())->toBe('handoff.pdf'); Storage::disk('local')->assertExists($message->attachmentPath()); }); test('file metadata is captured before temporary uploads are moved', function () { Storage::fake('local'); $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); $this->actingAs($user); $originalEnvironment = app()->environment(); $content = str_repeat('x', 131072); try { app()->instance('env', 'local'); Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id]) ->set('attachments', [ UploadedFile::fake()->createWithContent('screenshot.txt', $content), ]) ->call('sendMessage') ->assertHasNoErrors(); } finally { app()->instance('env', $originalEnvironment); } $message = Message::query() ->where('conversation_id', $conversation->id) ->where('type', Message::TypeFile) ->first(); expect($message)->not->toBeNull(); expect($message->metadata) ->toMatchArray([ 'original_name' => 'screenshot.txt', 'size' => 131072, ]); }); test('participants can download shared files', function () { Storage::fake('local'); $user = User::factory()->create(); $outsider = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); Storage::disk('local')->put('chat-attachments/'.$conversation->id.'/handoff.pdf', 'file contents'); $message = Message::factory() ->file([ 'path' => 'chat-attachments/'.$conversation->id.'/handoff.pdf', 'original_name' => 'handoff.pdf', 'mime_type' => 'application/pdf', 'size' => 13, ]) ->for($conversation) ->for($user, 'sender') ->create(); $this->actingAs($user) ->get(route('messages.attachment.download', $message)) ->assertOk() ->assertDownload('handoff.pdf'); $this->actingAs($outsider) ->get(route('messages.attachment.download', $message)) ->assertForbidden(); }); test('conversation details files action shows shared files', function () { $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); Message::factory() ->file([ 'original_name' => 'launch-plan.pdf', 'path' => 'chat-attachments/'.$conversation->id.'/launch-plan.pdf', 'size' => 42000, ]) ->for($conversation) ->for($user, 'sender') ->create(); $this->actingAs($user); Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id]) ->call('openFiles') ->assertSet('showFilesModal', true) ->assertSee('launch-plan.pdf') ->assertSee('Conversation files'); }); test('participants can mute and pin conversations from details', function () { $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); $participant = ConversationParticipant::factory()->for($conversation)->for($user)->create(); $this->actingAs($user); Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id]) ->call('toggleMute') ->assertSee('Muted') ->call('togglePin') ->assertSee('Pinned'); $participant->refresh(); expect($participant->muted_until)->not->toBeNull() ->and($participant->pinned_at)->not->toBeNull(); Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id]) ->call('toggleMute') ->assertSee('Mute') ->call('togglePin') ->assertSee('Pin'); $participant->refresh(); expect($participant->muted_until)->toBeNull() ->and($participant->pinned_at)->toBeNull(); }); test('pinned conversations stay at the top of the conversation list', function () { $user = User::factory()->create(); $pinnedTeammate = User::factory()->create(['name' => 'Priority Partner']); $regularTeammate = User::factory()->create(['name' => 'Regular Partner']); $pinnedConversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(['updated_at' => now()->subDay()]); $regularConversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(['updated_at' => now()]); ConversationParticipant::factory()->for($pinnedConversation)->for($user)->create(['pinned_at' => now()]); ConversationParticipant::factory()->for($pinnedConversation)->for($pinnedTeammate)->create(); ConversationParticipant::factory()->for($regularConversation)->for($user)->create(); ConversationParticipant::factory()->for($regularConversation)->for($regularTeammate)->create(); $this->actingAs($user); Livewire::test(ConversationList::class) ->assertSeeInOrder(['Priority Partner', 'Regular Partner']) ->assertSee('Pinned'); }); test('participants can add people to a direct conversation', function () { $user = User::factory()->create(); $teammate = User::factory()->create(['name' => 'Mina Partner']); $newMember = User::factory()->create(['name' => 'Nima Added']); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); ConversationParticipant::factory()->for($conversation)->for($teammate)->create(); $this->actingAs($user); Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id]) ->call('openAddMembers') ->set('groupName', 'Project Room') ->call('toggleMember', $newMember->id) ->call('addMembers') ->assertHasNoErrors(); $conversation->refresh(); expect($conversation->type)->toBe(Conversation::TypeGroup); expect($conversation->name)->toBe('Project Room'); expect($conversation->participants()->where('user_id', $newMember->id)->exists())->toBeTrue(); }); test('selected conversations render the stream header and details', function () { $user = User::factory()->create(); $teammate = User::factory()->create(['name' => 'Mina Partner']); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); ConversationParticipant::factory()->for($conversation)->for($teammate)->create(); Message::factory() ->for($conversation) ->for($teammate, 'sender') ->create(['body' => 'The latest prototype feels much faster.']); $this->actingAs($user); Livewire::test(ConversationView::class, ['conversationId' => $conversation->id]) ->assertSee('The latest prototype feels much faster.'); Livewire::test(ConversationHeader::class, ['conversationId' => $conversation->id]) ->assertSee('Mina Partner'); Livewire::test(ConversationDetailsPanel::class, ['conversationId' => $conversation->id]) ->assertSee('Mina Partner'); }); test('conversation header search button opens message search', function () { $user = User::factory()->create(); $teammate = User::factory()->create(['name' => 'Mina Partner']); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); ConversationParticipant::factory()->for($conversation)->for($teammate)->create(); $this->actingAs($user); Livewire::test(ConversationHeader::class, ['conversationId' => $conversation->id]) ->call('toggleSearch') ->assertDispatched('conversation-search-toggled'); Livewire::test(ConversationView::class, ['conversationId' => $conversation->id]) ->dispatch('conversation-search-toggled', conversationId: $conversation->id) ->assertSet('messageSearchOpen', true) ->assertSee('Search this conversation'); }); test('message search filters the selected conversation', function () { $user = User::factory()->create(); $teammate = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); ConversationParticipant::factory()->for($conversation)->for($teammate)->create(); Message::factory() ->for($conversation) ->for($teammate, 'sender') ->create(['body' => 'The deployment checklist is ready.']); Message::factory() ->for($conversation) ->for($teammate, 'sender') ->create(['body' => 'Budget review moved to Tuesday.']); $this->actingAs($user); Livewire::test(ConversationView::class, ['conversationId' => $conversation->id]) ->dispatch('conversation-search-toggled', conversationId: $conversation->id) ->set('messageSearch', 'deployment') ->assertSee('1 matching message') ->assertSee('deployment') ->assertDontSee('Budget review moved to Tuesday.'); }); test('empty messages are rejected', function () { $user = User::factory()->create(); $conversation = Conversation::factory() ->direct() ->for($user, 'creator') ->create(); ConversationParticipant::factory()->for($conversation)->for($user)->create(); $this->actingAs($user); Livewire::test(MessageComposer::class, ['conversationId' => $conversation->id]) ->set('body', ' ') ->call('sendMessage') ->assertHasErrors(['body' => 'required']); });