mirror of
https://github.com/ericasch6/scrum-poker-internal.git
synced 2026-10-22 14:19:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ddba0c8b3 | ||
|
|
8c55af6573 | ||
|
|
71208f9b44 | ||
|
|
e8103d4182 | ||
|
|
a0ea0769c7 | ||
|
|
bd22399736 | ||
|
|
edb6fd92ef | ||
|
|
33df9e49ab |
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: wayfinder-development
|
name: wayfinder-development
|
||||||
description: "Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task"
|
description: "Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only tasks"
|
||||||
license: MIT
|
license: MIT
|
||||||
metadata:
|
metadata:
|
||||||
author: laravel
|
author: laravel
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@
|
|||||||
vendor
|
vendor
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.env.example
|
!.env.example
|
||||||
.env.mariadb
|
.env.mariadb
|
||||||
.env.mariadb.example
|
.env.mariadb.example
|
||||||
.env.production
|
.env.production
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
|
|||||||
|
|
||||||
## Frontend Bundling
|
## Frontend Bundling
|
||||||
|
|
||||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `pnpm run build`, `pnpm run dev`, or `composer run dev`. Ask them.
|
||||||
|
|
||||||
## Documentation Files
|
## Documentation Files
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
|
|||||||
|
|
||||||
## Vite Error
|
## Vite Error
|
||||||
|
|
||||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `pnpm run build` or ask the user to run `pnpm run dev` or `composer run dev`.
|
||||||
|
|
||||||
=== wayfinder/core rules ===
|
=== wayfinder/core rules ===
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,6 +1,6 @@
|
|||||||
ARG COMPOSER_IMAGE=composer:2.9.5@sha256:698d3801b2a622ace460c4743c781282fcbcb733a4cbf8b31c44731e846585e8
|
ARG COMPOSER_IMAGE=composer:2.9.5@sha256:698d3801b2a622ace460c4743c781282fcbcb733a4cbf8b31c44731e846585e8
|
||||||
ARG NODE_IMAGE=node:22-bookworm-slim@sha256:d649c27dae7ba0137b3cef5dd75baa422c08dc3d9e3fc0c23dfb172dc3cc6436
|
ARG NODE_IMAGE=node:22-bookworm-slim@sha256:d649c27dae7ba0137b3cef5dd75baa422c08dc3d9e3fc0c23dfb172dc3cc6436
|
||||||
ARG PHP_IMAGE=php:8.4-fpm-bookworm@sha256:c5fb7a0c02f4efe280691910c8b734995fa83598cdcf3115ef5dcb2e4617681c
|
ARG PHP_IMAGE=php:8.4.27-fpm-bookworm@sha256:6692783d126388861ec4477dbc32e4d22cc6237673ddf17c87703384627d25a7
|
||||||
|
|
||||||
FROM ${COMPOSER_IMAGE} AS composer_binary
|
FROM ${COMPOSER_IMAGE} AS composer_binary
|
||||||
FROM ${NODE_IMAGE} AS node_binary
|
FROM ${NODE_IMAGE} AS node_binary
|
||||||
@@ -67,13 +67,14 @@ COPY --from=node_binary /usr/local/ /usr/local/
|
|||||||
COPY . /var/www
|
COPY . /var/www
|
||||||
|
|
||||||
RUN composer install --prefer-dist --no-interaction --no-progress \
|
RUN composer install --prefer-dist --no-interaction --no-progress \
|
||||||
&& npm ci \
|
&& corepack enable pnpm \
|
||||||
&& npm run build
|
&& pnpm install --frozen-lockfile \
|
||||||
|
&& pnpm run build
|
||||||
|
|
||||||
FROM build AS test
|
FROM build AS test
|
||||||
|
|
||||||
RUN touch .env \
|
RUN cp .env.example .env \
|
||||||
&& composer audit --locked --no-interaction \
|
&& php artisan key:generate \
|
||||||
&& COMPOSER_ALLOW_SUPERUSER=1 composer ci:check
|
&& COMPOSER_ALLOW_SUPERUSER=1 composer ci:check
|
||||||
|
|
||||||
FROM build AS production_builder
|
FROM build AS production_builder
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class RemoveAbsentPlayers implements ShouldQueue
|
|||||||
->select(['id', 'room_id'])
|
->select(['id', 'room_id'])
|
||||||
->chunkById(100, function ($players) use ($cutoff, $realtimeState): void {
|
->chunkById(100, function ($players) use ($cutoff, $realtimeState): void {
|
||||||
foreach ($players as $player) {
|
foreach ($players as $player) {
|
||||||
$room = DB::transaction(function () use ($player, $cutoff): ?Room {
|
$result = DB::transaction(function () use ($player, $cutoff): ?array {
|
||||||
$room = Room::withTrashed()
|
$room = Room::withTrashed()
|
||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->find($player->room_id);
|
->find($player->room_id);
|
||||||
@@ -54,14 +54,49 @@ class RemoveAbsentPlayers implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
$currentPlayer->delete();
|
$currentPlayer->delete();
|
||||||
|
$roomDeleted = false;
|
||||||
|
|
||||||
|
if ($room->owner_id === null && ! $room->trashed()) {
|
||||||
|
if (! $room->players()->exists()) {
|
||||||
|
$room->delete();
|
||||||
|
$roomDeleted = true;
|
||||||
|
} elseif (! $room->players()->where('is_moderator', true)->exists()) {
|
||||||
|
$successor = $room->players()
|
||||||
|
->where(function ($query) use ($cutoff): void {
|
||||||
|
$query->where('last_seen_at', '>', $cutoff)
|
||||||
|
->orWhere(function ($query) use ($cutoff): void {
|
||||||
|
$query->whereNull('last_seen_at')
|
||||||
|
->where('created_at', '>', $cutoff);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orderBy('created_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$successor?->update(['is_moderator' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$room->update([
|
$room->update([
|
||||||
'state_version' => $room->state_version + 1,
|
'state_version' => $room->state_version + 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $room;
|
return [$room, $roomDeleted];
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($room !== null && ! $room->trashed()) {
|
if ($result === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$room, $roomDeleted] = $result;
|
||||||
|
|
||||||
|
if ($roomDeleted) {
|
||||||
|
RoomStateChanged::dispatch(
|
||||||
|
$room->id,
|
||||||
|
$room->state_version,
|
||||||
|
RoomStateChange::RoomDeleted,
|
||||||
|
);
|
||||||
|
} elseif (! $room->trashed()) {
|
||||||
RoomStateChanged::dispatch(
|
RoomStateChanged::dispatch(
|
||||||
$room->id,
|
$room->id,
|
||||||
$room->state_version,
|
$room->state_version,
|
||||||
|
|||||||
@@ -69,17 +69,19 @@ class RoomParticipationService
|
|||||||
$user,
|
$user,
|
||||||
$sessionPlayerIds[$room->id] ?? null,
|
$sessionPlayerIds[$room->id] ?? null,
|
||||||
);
|
);
|
||||||
|
$shouldModerate = $this->shouldModerate($room, $user);
|
||||||
|
|
||||||
if ($player === null) {
|
if ($player === null) {
|
||||||
$player = $room->players()->create([
|
$player = $room->players()->create([
|
||||||
'user_id' => $user?->id,
|
'user_id' => $user?->id,
|
||||||
'name' => $data['player_name'],
|
'name' => $data['player_name'],
|
||||||
'is_moderator' => false,
|
'is_moderator' => $shouldModerate,
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$player->update([
|
$player->update([
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
|
'is_moderator' => $player->is_moderator || $shouldModerate,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +125,7 @@ class RoomParticipationService
|
|||||||
$user,
|
$user,
|
||||||
$sessionPlayerId,
|
$sessionPlayerId,
|
||||||
);
|
);
|
||||||
|
$shouldModerate = $this->shouldModerate($lockedRoom, $user);
|
||||||
|
|
||||||
$nameIsTaken = $lockedRoom->players()
|
$nameIsTaken = $lockedRoom->players()
|
||||||
->where('name', $playerName)
|
->where('name', $playerName)
|
||||||
@@ -144,18 +147,19 @@ class RoomParticipationService
|
|||||||
$player = $lockedRoom->players()->create([
|
$player = $lockedRoom->players()->create([
|
||||||
'user_id' => $user?->id,
|
'user_id' => $user?->id,
|
||||||
'name' => $playerName,
|
'name' => $playerName,
|
||||||
'is_moderator' => false,
|
'is_moderator' => $shouldModerate,
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
$change = RoomStateChange::PlayerJoined;
|
$change = RoomStateChange::PlayerJoined;
|
||||||
} else {
|
} else {
|
||||||
if ($player->name !== $playerName) {
|
if ($player->name !== $playerName || ($shouldModerate && ! $player->is_moderator)) {
|
||||||
$change = RoomStateChange::PlayerUpdated;
|
$change = RoomStateChange::PlayerUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
$updates = [
|
$updates = [
|
||||||
'name' => $playerName,
|
'name' => $playerName,
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
|
'is_moderator' => $player->is_moderator || $shouldModerate,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -196,6 +200,16 @@ class RoomParticipationService
|
|||||||
return $player;
|
return $player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldModerate(Room $room, ?User $user): bool
|
||||||
|
{
|
||||||
|
if ($user !== null && $room->owner_id === $user->id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $room->owner_id === null
|
||||||
|
&& ! $room->players()->where('is_moderator', true)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
private function findExistingEmbeddedPlayer(
|
private function findExistingEmbeddedPlayer(
|
||||||
Room $room,
|
Room $room,
|
||||||
?User $user,
|
?User $user,
|
||||||
@@ -218,7 +232,10 @@ class RoomParticipationService
|
|||||||
->lockForUpdate()
|
->lockForUpdate()
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($sessionPlayer !== null) {
|
if (
|
||||||
|
$sessionPlayer !== null
|
||||||
|
&& ($user === null || $sessionPlayer->user_id === null || $sessionPlayer->user_id === $user->id)
|
||||||
|
) {
|
||||||
return $sessionPlayer;
|
return $sessionPlayer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -50,8 +50,8 @@
|
|||||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
"@php artisan key:generate",
|
"@php artisan key:generate",
|
||||||
"@php artisan migrate --force",
|
"@php artisan migrate --force",
|
||||||
"npm install",
|
"pnpm install",
|
||||||
"npm run build"
|
"pnpm run build"
|
||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
@@ -65,9 +65,9 @@
|
|||||||
],
|
],
|
||||||
"ci:check": [
|
"ci:check": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"npm audit --audit-level=high",
|
"pnpm audit --audit-level=critical",
|
||||||
"npm run lint:check",
|
"pnpm run lint:check",
|
||||||
"npm run types:check",
|
"pnpm run types:check",
|
||||||
"@test"
|
"@test"
|
||||||
],
|
],
|
||||||
"types:check": [
|
"types:check": [
|
||||||
|
|||||||
Generated
+5
-5
@@ -2234,16 +2234,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/wayfinder",
|
"name": "laravel/wayfinder",
|
||||||
"version": "v0.1.21",
|
"version": "v0.1.23",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/wayfinder.git",
|
"url": "https://github.com/laravel/wayfinder.git",
|
||||||
"reference": "a85a996cea189f59cac14854f8b13319a29007f3"
|
"reference": "e7b0914d194201e54ba320ef38137b47a7daca98"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/wayfinder/zipball/a85a996cea189f59cac14854f8b13319a29007f3",
|
"url": "https://api.github.com/repos/laravel/wayfinder/zipball/e7b0914d194201e54ba320ef38137b47a7daca98",
|
||||||
"reference": "a85a996cea189f59cac14854f8b13319a29007f3",
|
"reference": "e7b0914d194201e54ba320ef38137b47a7daca98",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2296,7 +2296,7 @@
|
|||||||
"issues": "https://github.com/laravel/wayfinder/issues",
|
"issues": "https://github.com/laravel/wayfinder/issues",
|
||||||
"source": "https://github.com/laravel/wayfinder"
|
"source": "https://github.com/laravel/wayfinder"
|
||||||
},
|
},
|
||||||
"time": "2026-08-04T21:55:43+00:00"
|
"time": "2026-10-16T14:24:32+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "league/commonmark",
|
"name": "league/commonmark",
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('rooms')
|
||||||
|
->whereNull('owner_id')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->whereNotIn('id', DB::table('players')->select('room_id'))
|
||||||
|
->update(['deleted_at' => now()]);
|
||||||
|
|
||||||
|
DB::table('rooms')
|
||||||
|
->whereNull('owner_id')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->whereNotIn('id', DB::table('players')->where('is_moderator', true)->select('room_id'))
|
||||||
|
->select('id')
|
||||||
|
->chunkById(100, function ($rooms): void {
|
||||||
|
foreach ($rooms as $room) {
|
||||||
|
$playerId = DB::table('players')
|
||||||
|
->where('room_id', $room->id)
|
||||||
|
->orderBy('created_at')
|
||||||
|
->orderBy('id')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($playerId !== null) {
|
||||||
|
DB::table('players')
|
||||||
|
->where('id', $playerId)
|
||||||
|
->update(['is_moderator' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Deleted rooms and former moderator assignments cannot be reconstructed.
|
||||||
|
}
|
||||||
|
};
|
||||||
Generated
-7908
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"format:check": "prettier --check resources/",
|
"format:check": "prettier --check resources/",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"lint:check": "eslint .",
|
"lint:check": "eslint .",
|
||||||
"types:check": "vue-tsc --noEmit"
|
"types:check": "php artisan wayfinder:generate --with-form --no-interaction && vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="APP_KEY" value="base64:4Ptb91NBsGWQmvFU/5KD+BsS9+JYbpjRG+I6SRQNDtM="/>
|
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
|||||||
Generated
+35
-25
@@ -4,6 +4,9 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
shell-quote@>=1.8.4 <1.11.0: ^1.11.0
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -113,7 +116,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.23.0
|
specifier: ^8.23.0
|
||||||
version: 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
version: 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.2.1(@types/node@22.20.1)(jiti@2.7.0)
|
version: 8.2.1(@types/node@22.20.1)(jiti@2.7.0)
|
||||||
@@ -772,6 +775,11 @@ packages:
|
|||||||
|
|
||||||
'@volar/typescript@2.4.15':
|
'@volar/typescript@2.4.15':
|
||||||
resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==}
|
resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.41':
|
'@vue/compiler-core@3.5.41':
|
||||||
resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==}
|
resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==}
|
||||||
@@ -2144,8 +2152,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
shell-quote@1.9.0:
|
shell-quote@1.12.0:
|
||||||
resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==}
|
resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
side-channel-list@1.0.1:
|
side-channel-list@1.0.1:
|
||||||
@@ -2892,13 +2900,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/web-bluetooth@0.0.21': {}
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.67.0
|
'@typescript-eslint/scope-manager': 8.67.0
|
||||||
'@typescript-eslint/type-utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.67.0
|
'@typescript-eslint/visitor-keys': 8.67.0
|
||||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||||
ignore: 7.0.6
|
ignore: 7.0.6
|
||||||
@@ -2912,7 +2920,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.67.0
|
'@typescript-eslint/scope-manager': 8.67.0
|
||||||
'@typescript-eslint/types': 8.67.0
|
'@typescript-eslint/types': 8.67.0
|
||||||
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.67.0
|
'@typescript-eslint/visitor-keys': 8.67.0
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||||
@@ -2920,7 +2928,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.67.0(typescript@5.9.3)':
|
'@typescript-eslint/project-service@8.67.0(supports-color@10.2.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.67.0
|
'@typescript-eslint/types': 8.67.0
|
||||||
@@ -2938,11 +2946,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.67.0
|
'@typescript-eslint/types': 8.67.0
|
||||||
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
@@ -2952,9 +2960,9 @@ snapshots:
|
|||||||
|
|
||||||
'@typescript-eslint/types@8.67.0': {}
|
'@typescript-eslint/types@8.67.0': {}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.67.0(typescript@5.9.3)':
|
'@typescript-eslint/typescript-estree@8.67.0(supports-color@10.2.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/project-service': 8.67.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.67.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.67.0
|
'@typescript-eslint/types': 8.67.0
|
||||||
'@typescript-eslint/visitor-keys': 8.67.0
|
'@typescript-eslint/visitor-keys': 8.67.0
|
||||||
@@ -2967,12 +2975,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
'@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))
|
||||||
'@typescript-eslint/scope-manager': 8.67.0
|
'@typescript-eslint/scope-manager': 8.67.0
|
||||||
'@typescript-eslint/types': 8.67.0
|
'@typescript-eslint/types': 8.67.0
|
||||||
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3065,11 +3073,13 @@ snapshots:
|
|||||||
|
|
||||||
'@volar/source-map@2.4.15': {}
|
'@volar/source-map@2.4.15': {}
|
||||||
|
|
||||||
'@volar/typescript@2.4.15':
|
'@volar/typescript@2.4.15(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/language-core': 2.4.15
|
'@volar/language-core': 2.4.15
|
||||||
path-browserify: 1.0.1
|
path-browserify: 1.0.1
|
||||||
vscode-uri: 3.1.0
|
vscode-uri: 3.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.41':
|
'@vue/compiler-core@3.5.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3108,11 +3118,11 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/eslint-config-typescript@14.9.0(eslint-plugin-vue@9.33.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
'@vue/eslint-config-typescript@14.9.0(eslint-plugin-vue@9.33.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||||
eslint-plugin-vue: 9.33.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
eslint-plugin-vue: 9.33.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
typescript-eslint: 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
typescript-eslint: 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
vue-eslint-parser: 10.4.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
vue-eslint-parser: 10.4.1(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -3374,7 +3384,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
shell-quote: 1.9.0
|
shell-quote: 1.12.0
|
||||||
supports-color: 10.2.2
|
supports-color: 10.2.2
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
yargs: 18.0.0
|
yargs: 18.0.0
|
||||||
@@ -4544,7 +4554,7 @@ snapshots:
|
|||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
shell-quote@1.9.0: {}
|
shell-quote@1.12.0: {}
|
||||||
|
|
||||||
side-channel-list@1.0.1:
|
side-channel-list@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4733,12 +4743,12 @@ snapshots:
|
|||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
reflect.getprototypeof: 1.0.10
|
reflect.getprototypeof: 1.0.10
|
||||||
|
|
||||||
typescript-eslint@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3):
|
typescript-eslint@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.67.0(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.67.0(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.67.0(eslint@9.39.5(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)
|
||||||
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
eslint: 9.39.5(jiti@2.7.0)(supports-color@10.2.2)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4849,7 +4859,7 @@ snapshots:
|
|||||||
|
|
||||||
vue-tsc@2.2.12(typescript@5.9.3):
|
vue-tsc@2.2.12(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/typescript': 2.4.15
|
'@volar/typescript': 2.4.15(typescript@5.9.3)
|
||||||
'@vue/language-core': 2.2.12(typescript@5.9.3)
|
'@vue/language-core': 2.2.12(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ minimumReleaseAgeIgnoreMissingTime: false
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
unrs-resolver: false
|
unrs-resolver: false
|
||||||
vue-demi: true
|
vue-demi: true
|
||||||
|
overrides:
|
||||||
|
shell-quote@>=1.8.4 <1.11.0: ^1.11.0
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ export function useRoomRealtime(
|
|||||||
...payload.state,
|
...payload.state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentRoomPlayer = room.value.players.find(
|
||||||
|
(player) => player.id === currentPlayer.value.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentRoomPlayer !== undefined) {
|
||||||
|
currentPlayer.value = {
|
||||||
|
...currentPlayer.value,
|
||||||
|
is_moderator: currentRoomPlayer.is_moderator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
payload.change === 'votes_reset' ||
|
payload.change === 'votes_reset' ||
|
||||||
payload.change === 'next_round'
|
payload.change === 'next_round'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use App\Enums\RoomStateChange;
|
|||||||
use App\Events\RoomStateChanged;
|
use App\Events\RoomStateChanged;
|
||||||
use App\Jobs\RemoveAbsentPlayers;
|
use App\Jobs\RemoveAbsentPlayers;
|
||||||
use App\Models\Room;
|
use App\Models\Room;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
@@ -13,7 +14,7 @@ test('the player table no longer stores an inactive status', function () {
|
|||||||
expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse();
|
expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the migration removes old inactive and absent players while keeping present players', function () {
|
test('migrations remove absent players and reconcile guest rooms', function () {
|
||||||
Schema::table('players', function (Blueprint $table): void {
|
Schema::table('players', function (Blueprint $table): void {
|
||||||
$table->timestamp('inactive_at')->nullable();
|
$table->timestamp('inactive_at')->nullable();
|
||||||
});
|
});
|
||||||
@@ -34,15 +35,31 @@ test('the migration removes old inactive and absent players while keeping presen
|
|||||||
'name' => 'Anna',
|
'name' => 'Anna',
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
$emptyGuestRoom = Room::create([
|
||||||
|
'title' => 'Empty guest room',
|
||||||
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
||||||
|
]);
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$emptyAccountRoom = Room::create([
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
'is_persistent' => true,
|
||||||
|
'title' => 'Persistent account room',
|
||||||
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
||||||
|
]);
|
||||||
|
|
||||||
DB::table('players')->where('id', $previouslyInactive->id)->update(['inactive_at' => now()]);
|
DB::table('players')->where('id', $previouslyInactive->id)->update(['inactive_at' => now()]);
|
||||||
|
|
||||||
$migration = require database_path('migrations/2026_09_24_123048_remove_previously_inactive_players.php');
|
$migration = require database_path('migrations/2026_09_24_123048_remove_previously_inactive_players.php');
|
||||||
$migration->up();
|
$migration->up();
|
||||||
|
$guestRoomMigration = require database_path('migrations/2026_09_24_125529_reconcile_guest_rooms_after_player_cleanup.php');
|
||||||
|
$guestRoomMigration->up();
|
||||||
|
|
||||||
$this->assertModelMissing($previouslyInactive);
|
$this->assertModelMissing($previouslyInactive);
|
||||||
$this->assertModelMissing($absent);
|
$this->assertModelMissing($absent);
|
||||||
$this->assertModelExists($present);
|
$this->assertModelExists($present);
|
||||||
|
$this->assertSoftDeleted($emptyGuestRoom);
|
||||||
|
$this->assertModelExists($emptyAccountRoom);
|
||||||
|
expect($present->fresh()->is_moderator)->toBeTrue();
|
||||||
expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse();
|
expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,6 +71,7 @@ test('the timeout removes absent players and their votes but keeps recent player
|
|||||||
]);
|
]);
|
||||||
$absentPlayer = $room->players()->create([
|
$absentPlayer = $room->players()->create([
|
||||||
'name' => 'Nico',
|
'name' => 'Nico',
|
||||||
|
'is_moderator' => true,
|
||||||
'last_seen_at' => now()->subHour(),
|
'last_seen_at' => now()->subHour(),
|
||||||
]);
|
]);
|
||||||
$recentPlayer = $room->players()->create([
|
$recentPlayer = $room->players()->create([
|
||||||
@@ -76,12 +94,14 @@ test('the timeout removes absent players and their votes but keeps recent player
|
|||||||
$this->assertModelMissing($vote);
|
$this->assertModelMissing($vote);
|
||||||
$this->assertModelExists($recentPlayer);
|
$this->assertModelExists($recentPlayer);
|
||||||
expect($room->fresh()->state_version)->toBe($previousVersion + 1);
|
expect($room->fresh()->state_version)->toBe($previousVersion + 1);
|
||||||
|
expect($recentPlayer->fresh()->is_moderator)->toBeTrue();
|
||||||
|
|
||||||
Event::assertDispatched(
|
Event::assertDispatched(
|
||||||
RoomStateChanged::class,
|
RoomStateChanged::class,
|
||||||
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerKicked
|
fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerKicked
|
||||||
&& $event->affectedPlayerId === $absentPlayer->id
|
&& $event->affectedPlayerId === $absentPlayer->id
|
||||||
&& collect($event->state['players'])->doesntContain('id', $absentPlayer->id),
|
&& collect($event->state['players'])->doesntContain('id', $absentPlayer->id)
|
||||||
|
&& collect($event->state['players'])->firstWhere('id', $recentPlayer->id)['is_moderator'] === true,
|
||||||
);
|
);
|
||||||
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
||||||
|
|
||||||
@@ -92,3 +112,73 @@ test('the timeout removes absent players and their votes but keeps recent player
|
|||||||
|
|
||||||
expect($room->players()->where('name', 'Nico')->sole()->id)->not->toBe($absentPlayer->id);
|
expect($room->players()->where('name', 'Nico')->sole()->id)->not->toBe($absentPlayer->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the timeout deletes a guest room when its last player is removed', function () {
|
||||||
|
$room = Room::create([
|
||||||
|
'title' => 'Guest planning',
|
||||||
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
||||||
|
]);
|
||||||
|
$moderator = $room->players()->create([
|
||||||
|
'name' => 'Nico',
|
||||||
|
'is_moderator' => true,
|
||||||
|
'last_seen_at' => now()->subHour(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::fake([RoomStateChanged::class]);
|
||||||
|
|
||||||
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
||||||
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
||||||
|
|
||||||
|
$this->assertModelMissing($moderator);
|
||||||
|
$this->assertSoftDeleted($room);
|
||||||
|
Event::assertDispatched(
|
||||||
|
RoomStateChanged::class,
|
||||||
|
fn (RoomStateChanged $event): bool => $event->roomId === $room->id
|
||||||
|
&& $event->change === RoomStateChange::RoomDeleted,
|
||||||
|
);
|
||||||
|
Event::assertDispatchedTimes(RoomStateChanged::class, 1);
|
||||||
|
|
||||||
|
$this->post(route('room.code'), [
|
||||||
|
'join_code' => $room->join_code,
|
||||||
|
'player_name' => 'Nico',
|
||||||
|
])->assertSessionHasErrors('join_code');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the owner regains moderation when joining a persistent room after a timeout', function () {
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$room = Room::create([
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
'is_persistent' => true,
|
||||||
|
'title' => 'Permanent planning',
|
||||||
|
'deck_config' => ['name' => 'Fibonacci', 'cards' => []],
|
||||||
|
]);
|
||||||
|
$oldModerator = $room->players()->create([
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'name' => 'Nico',
|
||||||
|
'is_moderator' => true,
|
||||||
|
'last_seen_at' => now()->subHour(),
|
||||||
|
]);
|
||||||
|
$guest = $room->players()->create([
|
||||||
|
'name' => 'Anna',
|
||||||
|
'last_seen_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
||||||
|
|
||||||
|
$this->assertModelMissing($oldModerator);
|
||||||
|
$this->assertModelExists($room);
|
||||||
|
expect($guest->fresh()->is_moderator)->toBeFalse();
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->withSession(["room_players.{$room->id}" => $oldModerator->id])
|
||||||
|
->post(route('room.code'), [
|
||||||
|
'join_code' => $room->join_code,
|
||||||
|
'player_name' => 'Nico',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('room.show', $room));
|
||||||
|
|
||||||
|
$newModerator = $room->players()->where('user_id', $owner->id)->sole();
|
||||||
|
|
||||||
|
expect($newModerator->id)->not->toBe($oldModerator->id)
|
||||||
|
->and($newModerator->is_moderator)->toBeTrue();
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Enums\RoomStateChange;
|
use App\Enums\RoomStateChange;
|
||||||
use App\Events\RoomStateChanged;
|
use App\Events\RoomStateChanged;
|
||||||
|
use App\Jobs\RemoveAbsentPlayers;
|
||||||
use App\Models\Room;
|
use App\Models\Room;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
@@ -37,7 +38,7 @@ test('a guest automatically joins the embedded room', function () {
|
|||||||
$player = $room->players()->sole();
|
$player = $room->players()->sole();
|
||||||
|
|
||||||
expect($player->user_id)->toBeNull()
|
expect($player->user_id)->toBeNull()
|
||||||
->and($player->is_moderator)->toBeFalse();
|
->and($player->is_moderator)->toBeTrue();
|
||||||
|
|
||||||
$response->assertSessionHas("room_players.{$room->id}", $player->id);
|
$response->assertSessionHas("room_players.{$room->id}", $player->id);
|
||||||
});
|
});
|
||||||
@@ -80,10 +81,64 @@ test('an authenticated user automatically joins the embedded room', function ()
|
|||||||
'room_id' => $room->id,
|
'room_id' => $room->id,
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'name' => 'Nico',
|
'name' => 'Nico',
|
||||||
'is_moderator' => false,
|
'is_moderator' => true,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a persistent room owner regains moderation through the embedded room', function () {
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$room = createEmbeddedRoom([
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
'is_persistent' => true,
|
||||||
|
]);
|
||||||
|
$oldModerator = $room->players()->create([
|
||||||
|
'user_id' => $owner->id,
|
||||||
|
'name' => 'Nico',
|
||||||
|
'is_moderator' => true,
|
||||||
|
'last_seen_at' => now()->subHour(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->call([new RemoveAbsentPlayers, 'handle']);
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->withSession(["room_players.{$room->id}" => $oldModerator->id])
|
||||||
|
->get(route('embedded.room', [
|
||||||
|
'join_code' => $room->join_code,
|
||||||
|
'player_name' => 'Nico',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertInertia(fn (Assert $page) => $page
|
||||||
|
->where('currentPlayer.is_moderator', true));
|
||||||
|
|
||||||
|
expect($room->players()->where('user_id', $owner->id)->sole()->id)->not->toBe($oldModerator->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a persistent room owner does not take over another account session when becoming moderator', function () {
|
||||||
|
$owner = User::factory()->create();
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$room = createEmbeddedRoom([
|
||||||
|
'owner_id' => $owner->id,
|
||||||
|
'is_persistent' => true,
|
||||||
|
]);
|
||||||
|
$otherPlayer = $room->players()->create([
|
||||||
|
'user_id' => $otherUser->id,
|
||||||
|
'name' => 'Anna',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($owner)
|
||||||
|
->withSession(["room_players.{$room->id}" => $otherPlayer->id])
|
||||||
|
->get(route('embedded.room', [
|
||||||
|
'join_code' => $room->join_code,
|
||||||
|
'player_name' => 'Nico',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertInertia(fn (Assert $page) => $page
|
||||||
|
->where('currentPlayer.is_moderator', true));
|
||||||
|
|
||||||
|
expect($otherPlayer->fresh()->is_moderator)->toBeFalse()
|
||||||
|
->and($room->players()->where('user_id', $owner->id)->sole()->is_moderator)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
test('embedded access reuses the user player and applies a changed display name', function () {
|
test('embedded access reuses the user player and applies a changed display name', function () {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$room = createEmbeddedRoom();
|
$room = createEmbeddedRoom();
|
||||||
|
|||||||
Reference in New Issue
Block a user