$owner->id, 'title' => 'Team Planning', 'current_round' => 1, 'deck_config' => [ 'name' => 'Fibonacci', 'cards' => [ ['key' => '3', 'label' => '3'], ['key' => '5', 'label' => '5'], ], ], ...$attributes, ]); } function addPlayerToKickRoom( Room $room, string $name, ?User $user = null, bool $isModerator = false, ): Player { return $room->players()->create([ 'user_id' => $user?->id, 'name' => $name, 'is_moderator' => $isModerator, 'last_seen_at' => now(), ]); } test('the room owner sees active room players on the dashboard', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); $moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true); $participant = addPlayerToKickRoom($room, 'Anna'); addPlayerToKickRoom($room, 'Inactive')->update(['inactive_at' => now()]); $this->actingAs($owner) ->get(route('dashboard')) ->assertInertia(fn (Assert $page) => $page ->has('rooms.0.players', 2) ->where('rooms.0.players.0.id', $moderator->id) ->where('rooms.0.players.0.is_moderator', true) ->where('rooms.0.players.0.is_owner', true) ->where('rooms.0.players.1.id', $participant->id) ->where('rooms.0.players.1.is_moderator', false) ->where('rooms.0.players.1.is_owner', false)); }); test('the room owner can kick an active participant', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); addPlayerToKickRoom($room, 'Moderator', $owner, true); $participant = addPlayerToKickRoom($room, 'Anna'); $remainingPlayer = addPlayerToKickRoom($room, 'Ben'); $remainingPlayer->votes()->create([ 'room_id' => $room->id, 'round' => $room->current_round, 'value' => '5', ]); $previousVersion = $room->fresh()->state_version; Event::fake([RoomStateChanged::class]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $participant])) ->assertRedirect(); expect($participant->fresh()->inactive_at)->not->toBeNull() ->and($room->fresh()->state_version)->toBe($previousVersion + 1); Event::assertDispatched( RoomStateChanged::class, function (RoomStateChanged $event) use ($participant, $remainingPlayer, $room): bool { if ( $event->roomId !== $room->id || $event->change !== RoomStateChange::PlayerKicked || $event->affectedPlayerId !== $participant->id || $event->state === null ) { return false; } $remainingPlayerState = collect($event->state['players']) ->firstWhere('id', $remainingPlayer->id); return collect($event->state['players'])->doesntContain('id', $participant->id) && $remainingPlayerState['has_voted'] === true && $remainingPlayerState['vote'] === null && $event->broadcastWith()['affected_player_id'] === $participant->id; }, ); Event::assertDispatchedTimes(RoomStateChanged::class, 1); }); test('a participant cannot kick another player', function () { $owner = User::factory()->create(); $participantUser = User::factory()->create(); $room = createRoomForPlayerKick($owner); addPlayerToKickRoom($room, 'Moderator', $owner, true); addPlayerToKickRoom($room, 'Anna', $participantUser); $target = addPlayerToKickRoom($room, 'Ben'); Event::fake([RoomStateChanged::class]); $this->actingAs($participantUser) ->delete(route('room.player.destroy', [$room, $target])) ->assertForbidden(); expect($target->fresh()->inactive_at)->toBeNull(); Event::assertNotDispatched(RoomStateChanged::class); }); test('the room owner cannot kick their own player', function () { $owner = User::factory()->create(); $room = createRoomForPlayerKick($owner); $moderator = addPlayerToKickRoom($room, 'Moderator', $owner, true); Event::fake([RoomStateChanged::class]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $moderator])) ->assertForbidden(); expect($moderator->fresh()->inactive_at)->toBeNull(); Event::assertNotDispatched(RoomStateChanged::class); }); test('the room owner cannot kick a player from another room', function () { $owner = User::factory()->create(); $otherOwner = User::factory()->create(); $room = createRoomForPlayerKick($owner); $otherRoom = createRoomForPlayerKick($otherOwner, ['title' => 'Other room']); $target = addPlayerToKickRoom($otherRoom, 'Anna'); Event::fake([RoomStateChanged::class]); $this->actingAs($owner) ->delete(route('room.player.destroy', [$room, $target])) ->assertForbidden(); expect($target->fresh()->inactive_at)->toBeNull(); Event::assertNotDispatched(RoomStateChanged::class); });