Implement instant websocket room updates

This commit is contained in:
2026-08-25 22:23:38 +02:00
parent bfc93dceba
commit d6e5455e90
18 changed files with 303 additions and 130 deletions
+39 -7
View File
@@ -5,6 +5,7 @@ use App\Events\RoomStateChanged;
use App\Jobs\CloseInactiveRooms;
use App\Models\Room;
use App\Models\User;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Event;
@@ -23,18 +24,20 @@ function createBroadcastRoom(array $attributes = []): Room
]);
}
test('room state broadcasts expose only invalidation metadata', function () {
test('room state broadcasts synchronously expose sanitized state', function () {
$event = new RoomStateChanged(
'019b8f80-37c4-71a3-b415-5f2697332c16',
7,
RoomStateChange::VoteChanged,
);
expect($event->broadcastAs())->toBe('room.state.changed')
expect($event)->toBeInstanceOf(ShouldBroadcastNow::class)
->and($event->broadcastAs())->toBe('room.state.changed')
->and($event->broadcastOn()->name)->toBe('rooms.019b8f80-37c4-71a3-b415-5f2697332c16')
->and($event->broadcastWith())->toBe([
'version' => 7,
'change' => 'vote_changed',
'state' => null,
])
->and($event->broadcastWith())->not->toHaveKeys([
'player_name',
@@ -68,23 +71,23 @@ test('voting and round actions broadcast their state changes', function () {
$this->withSession($session)
->post(route('room.vote.store', $room), ['value' => '5'])
->assertRedirect();
->assertNoContent();
$this->withSession($session)
->post(route('room.reveal', $room))
->assertRedirect();
->assertNoContent();
$this->withSession($session)
->post(route('room.next-round', $room))
->assertRedirect();
->assertNoContent();
$this->withSession($session)
->post(route('room.vote.store', $room), ['value' => '3'])
->assertRedirect();
->assertNoContent();
$this->withSession($session)
->post(route('room.reset', $room))
->assertRedirect();
->assertNoContent();
foreach ([
RoomStateChange::VoteChanged,
@@ -98,6 +101,35 @@ test('voting and round actions broadcast their state changes', function () {
);
}
Event::assertDispatched(
RoomStateChanged::class,
function (RoomStateChanged $event) use ($moderator): bool {
if ($event->change !== RoomStateChange::VoteChanged || $event->state === null) {
return false;
}
$player = collect($event->state['players'])->firstWhere('id', $moderator->id);
return $event->state['revealed'] === false
&& $player['has_voted'] === true
&& $player['vote'] === null;
},
);
Event::assertDispatched(
RoomStateChanged::class,
function (RoomStateChanged $event) use ($moderator): bool {
if ($event->change !== RoomStateChange::CardsRevealed || $event->state === null) {
return false;
}
$player = collect($event->state['players'])->firstWhere('id', $moderator->id);
return $event->state['revealed'] === true
&& $player['vote'] === '5';
},
);
Event::assertDispatchedTimes(RoomStateChanged::class, 5);
});
+1 -1
View File
@@ -238,7 +238,7 @@ test('meaningful game actions update the explicit activity timestamp', function
$this->withSession(["room_players.{$room->id}" => $moderator->id])
->post(route('room.vote.store', $room), ['value' => '5'])
->assertRedirect();
->assertNoContent();
expect($room->fresh()->last_activity_at->equalTo(now()))->toBeTrue();
});
+5 -5
View File
@@ -40,7 +40,7 @@ test('a participant can vote and change their vote in the current round', functi
$this->withSession($session)
->post(route('room.vote.store', $room), ['value' => '5'])
->assertRedirect();
->assertNoContent();
$this->assertDatabaseHas('votes', [
'room_id' => $room->id,
@@ -51,7 +51,7 @@ test('a participant can vote and change their vote in the current round', functi
$this->withSession($session)
->post(route('room.vote.store', $room), ['value' => '8'])
->assertRedirect();
->assertNoContent();
expect(Vote::query()->count())->toBe(1);
expect(Vote::query()->value('value'))->toBe('8')
@@ -91,7 +91,7 @@ test('other votes stay hidden until the moderator reveals the round', function (
$this->withSession($session)
->post(route('room.reveal', $room))
->assertRedirect();
->assertNoContent();
expect($room->fresh()->last_activity_at->greaterThan($previousActivity))->toBeTrue();
@@ -123,7 +123,7 @@ test('starting the next round clears the revealed state', function () {
$this->withSession(["room_players.{$room->id}" => $moderator->id])
->post(route('room.next-round', $room))
->assertRedirect();
->assertNoContent();
$room->refresh();
@@ -156,7 +156,7 @@ test('the moderator can reset only the votes from the current round', function (
$this->withSession(["room_players.{$room->id}" => $moderator->id])
->post(route('room.reset', $room))
->assertRedirect();
->assertNoContent();
$this->assertDatabaseHas('votes', [
'room_id' => $room->id,