'Sprint Planning', 'deck_config' => [ 'name' => 'Fibonacci', 'cards' => [ ['key' => '3', 'label' => '3'], ['key' => '5', 'label' => '5'], ['key' => '8', 'label' => '8'], ], ], ]); $moderator = $room->players()->create([ 'name' => 'Eric', 'is_moderator' => true, ]); $player = $room->players()->create([ 'name' => 'Anna', 'is_moderator' => false, ]); return [$room, $moderator, $player]; } test('a participant can vote and change their vote in the current round', function () { [$room, $moderator] = createVotingRoom(); $room->update(['last_activity_at' => now()->subDay()]); $previousActivity = $room->last_activity_at; $session = ["room_players.{$room->id}" => $moderator->id]; $this->withSession($session) ->post(route('room.vote.store', $room), ['value' => '5']) ->assertNoContent(); $this->assertDatabaseHas('votes', [ 'room_id' => $room->id, 'player_id' => $moderator->id, 'round' => 1, 'value' => '5', ]); $this->withSession($session) ->post(route('room.vote.store', $room), ['value' => '8']) ->assertNoContent(); expect(Vote::query()->count())->toBe(1); expect(Vote::query()->value('value'))->toBe('8') ->and($room->fresh()->last_activity_at->greaterThan($previousActivity))->toBeTrue(); }); test('a participant cannot vote with a card outside the room deck', function () { [$room, $moderator] = createVotingRoom(); $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->post(route('room.vote.store', $room), ['value' => '100']) ->assertSessionHasErrors('value'); $this->assertDatabaseCount('votes', 0); }); test('other votes stay hidden until the moderator reveals the round', function () { [$room, $moderator, $player] = createVotingRoom(); $room->update(['last_activity_at' => now()->subDay()]); $previousActivity = $room->last_activity_at; $player->votes()->create([ 'room_id' => $room->id, 'round' => 1, 'value' => '8', ]); $session = ["room_players.{$room->id}" => $moderator->id]; $this->withSession($session) ->get(route('room.show', $room)) ->assertInertia(fn (Assert $page) => $page ->component('rooms/Show') ->where('room.revealed', false) ->where('room.players.1.has_voted', true) ->where('room.players.1.vote', null)); $this->withSession($session) ->post(route('room.reveal', $room)) ->assertNoContent(); expect($room->fresh()->last_activity_at->greaterThan($previousActivity))->toBeTrue(); $this->withSession($session) ->get(route('room.show', $room)) ->assertInertia(fn (Assert $page) => $page ->where('room.revealed', true) ->where('room.players.1.vote', '8')); }); test('round controls remain moderator-only by default', function () { [$room, , $player] = createVotingRoom(); $session = ["room_players.{$room->id}" => $player->id]; $this->withSession($session) ->post(route('room.reveal', $room)) ->assertForbidden(); $this->withSession($session) ->post(route('room.next-round', $room)) ->assertForbidden(); $this->withSession($session) ->post(route('room.reset', $room)) ->assertForbidden(); expect($room->fresh()->revealed_at)->toBeNull(); }); test('the moderator can allow every participant to control the round', function () { [$room, $moderator, $player] = createVotingRoom(); $room->refresh(); $initialStateVersion = $room->state_version; $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->patch(route('room.permissions.update', $room), [ 'allow_participant_controls' => true, ]) ->assertNoContent(); $room->refresh(); expect($room->allow_participant_controls)->toBeTrue() ->and($room->state_version)->toBe($initialStateVersion + 1); $session = ["room_players.{$room->id}" => $player->id]; $this->withSession($session) ->get(route('room.show', $room)) ->assertInertia(fn (Assert $page) => $page ->where('room.allow_participant_controls', true)); $this->withSession($session) ->post(route('room.reveal', $room)) ->assertNoContent(); $this->withSession($session) ->post(route('room.next-round', $room)) ->assertNoContent(); $this->withSession($session) ->post(route('room.vote.store', $room), ['value' => '5']) ->assertNoContent(); $this->withSession($session) ->post(route('room.reset', $room)) ->assertNoContent(); expect($room->fresh()->current_round)->toBe(2); $this->assertDatabaseMissing('votes', [ 'room_id' => $room->id, 'round' => 2, ]); }); test('participants cannot change room permissions', function () { [$room, , $player] = createVotingRoom(); $this->withSession(["room_players.{$room->id}" => $player->id]) ->patch(route('room.permissions.update', $room), [ 'allow_participant_controls' => true, ]) ->assertForbidden(); expect($room->fresh()->allow_participant_controls)->toBeFalse(); }); test('room permissions require a strict boolean value', function () { [$room, $moderator] = createVotingRoom(); $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->patchJson(route('room.permissions.update', $room), [ 'allow_participant_controls' => 'true', ]) ->assertJsonValidationErrors('allow_participant_controls'); expect($room->fresh()->allow_participant_controls)->toBeFalse(); }); test('starting the next round clears the revealed state', function () { [$room, $moderator] = createVotingRoom(); $room->update([ 'revealed_at' => now(), 'last_activity_at' => now()->subDay(), ]); $previousActivity = $room->last_activity_at; $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->post(route('room.next-round', $room)) ->assertNoContent(); $room->refresh(); expect($room->current_round)->toBe(2) ->and($room->revealed_at)->toBeNull() ->and($room->last_activity_at->greaterThan($previousActivity))->toBeTrue(); }); test('the moderator can reset only the votes from the current round', function () { [$room, $moderator, $player] = createVotingRoom(); $room->update([ 'current_round' => 2, 'revealed_at' => now(), 'last_activity_at' => now()->subDay(), ]); $previousActivity = $room->last_activity_at; $player->votes()->create([ 'room_id' => $room->id, 'round' => 1, 'value' => '3', ]); $player->votes()->create([ 'room_id' => $room->id, 'round' => 2, 'value' => '8', ]); $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->post(route('room.reset', $room)) ->assertNoContent(); $this->assertDatabaseHas('votes', [ 'room_id' => $room->id, 'player_id' => $player->id, 'round' => 1, 'value' => '3', ]); $this->assertDatabaseMissing('votes', [ 'room_id' => $room->id, 'round' => 2, ]); $room->refresh(); expect($room->revealed_at)->toBeNull() ->and($room->last_activity_at->greaterThan($previousActivity))->toBeTrue(); }); test('revealed rounds show the average and median of numeric votes', function () { [$room, $moderator, $player] = createVotingRoom(); $thirdPlayer = $room->players()->create([ 'name' => 'Nina', 'is_moderator' => false, ]); $nonNumericPlayer = $room->players()->create([ 'name' => 'Coffee', 'is_moderator' => false, ]); foreach ([ [$moderator, '3'], [$player, '5'], [$thirdPlayer, '8'], [$nonNumericPlayer, 'coffee'], ] as [$votingPlayer, $value]) { $votingPlayer->votes()->create([ 'room_id' => $room->id, 'round' => 1, 'value' => $value, ]); } $room->update(['revealed_at' => now()]); $this->withSession(["room_players.{$room->id}" => $moderator->id]) ->get(route('room.show', $room)) ->assertInertia(fn (Assert $page) => $page ->where('room.statistics.average', '5,33') ->where('room.statistics.median', '5,00')); });