Skip to content

Commit 511f856

Browse files
committed
fix: bind live-notification listeners at setup time, not inside onMounted
Manual end-to-end verification (Task 12) with a real Reverb server and real browser sessions found that Dashboard.vue and Thread.vue never actually received live MessageCreated/ThreadCreated updates: calling useEchoNotification() from inside onMounted() meant its own internal onMounted/notification-listener registration silently never bound. Moving the call to top-level <script setup> scope fixes it. Also found that ThreadCreated's payload was missing participants' user data live (though correct on a fresh page load): the notification's Thread model loses relations attached only via setRelation() once it round-trips through queue serialization (SerializesModels), even on the sync queue connection. Fixed by explicitly reloading participants.user/messages.user in toArray(), and added a regression test that reproduces the round-trip instead of relying on Notification::fake(), which bypasses it and would not have caught this.
1 parent 61229e8 commit 511f856

4 files changed

Lines changed: 41 additions & 16 deletions

File tree

app/Notifications/ThreadCreated.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function via($notifiable)
4646
public function toArray($notifiable)
4747
{
4848
return [
49-
'payload' => (new ThreadResource($this->thread))->resolve(),
49+
'payload' => (new ThreadResource($this->thread->load('participants.user', 'messages.user')))->resolve(),
5050
];
5151
}
5252

resources/js/pages/Dashboard.vue

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup>
2-
import { ref, onMounted } from 'vue'
2+
import { ref } from 'vue'
33
import { useEchoNotification } from '@laravel/echo-vue'
44
import AppHeader from '../components/organisms/AppHeader.vue'
55
import ThreadList from '../components/organisms/ThreadList.vue'
@@ -27,11 +27,7 @@ function transformThread(payload) {
2727
}
2828
}
2929
30-
onMounted(() => {
31-
if (!props.auth) {
32-
return
33-
}
34-
30+
if (props.auth) {
3531
useEchoNotification(`App.Models.User.${props.auth.id}`, (notification) => {
3632
const payload = notification.payload
3733
const type = notification.type
@@ -64,7 +60,7 @@ onMounted(() => {
6460
threads.value = next
6561
}
6662
})
67-
})
63+
}
6864
</script>
6965
7066
<template>

resources/js/pages/Thread.vue

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup>
2-
import { ref, onMounted, computed } from 'vue'
2+
import { ref, computed } from 'vue'
33
import { useEchoNotification } from '@laravel/echo-vue'
44
import AppHeader from '../components/organisms/AppHeader.vue'
55
import AppAvatar from '../components/atoms/AppAvatar.vue'
@@ -30,21 +30,17 @@ function transform(payload) {
3030
}
3131
}
3232
33-
onMounted(() => {
34-
if (!props.auth) {
35-
return
36-
}
37-
33+
if (props.auth) {
3834
useEchoNotification(`App.Models.User.${props.auth.id}`, (notification) => {
3935
const payload = notification.payload
4036
41-
if (payload?.attributes?.thread_id === props.thread.id) {
37+
if (notification.type === 'App\\Notifications\\MessageCreated' && payload?.attributes?.thread_id === props.thread.id) {
4238
if (!messages.value.some((m) => m.id === payload.id)) {
4339
messages.value.push(transform(payload))
4440
}
4541
}
4642
})
47-
})
43+
}
4844
</script>
4945
5046
<template>

tests/Unit/MessageTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,4 +447,37 @@ public function test_notifications_carry_eager_loaded_user_relation_in_payload()
447447
return Arr::get($payload, 'payload.attributes.user.attributes.name') === $newParticipant->name;
448448
});
449449
}
450+
451+
/**
452+
* Reproduces the real dispatch path: ShouldQueue notifications are
453+
* serialized and restored via SerializesModels before toArray() runs
454+
* (this happens even on the "sync" queue connection), which drops any
455+
* relation set only via setRelation() and never actually eager-loaded.
456+
* ThreadCreated::toArray() must reload participants.user itself rather
457+
* than assume the caller's in-memory relations survive that round-trip.
458+
*
459+
* @return void
460+
*/
461+
public function test_thread_created_toarray_reloads_participant_user_after_serialization_round_trip()
462+
{
463+
$sender = User::factory()->create(['notify_via' => ['broadcast']]);
464+
$recipient = User::factory()->create(['notify_via' => ['broadcast']]);
465+
466+
$thread = $this->service->newThread('Serialization Round Trip', $sender, $this->envelope(), [$recipient->id]);
467+
468+
// Fetch a bare copy with participants loaded but participants.user NOT
469+
// loaded, mirroring what SerializesModels restores after unserialize().
470+
$bareThread = Thread::with('participants')->findOrFail($thread->id);
471+
472+
$notification = new ThreadCreated($bareThread);
473+
$payload = $this->resolvedPayload($notification->toArray($recipient));
474+
475+
/** @var array<int, array<string, mixed>> $participants */
476+
$participants = Arr::get($payload, 'payload.attributes.participants', []);
477+
$participantUserNames = collect($participants)
478+
->pluck('attributes.user.attributes.name')
479+
->filter();
480+
481+
$this->assertCount(2, $participantUserNames);
482+
}
450483
}

0 commit comments

Comments
 (0)