diff --git a/app/Notifications/NewActivityNotification.php b/app/Notifications/NewActivityNotification.php
index fbb2834..1683743 100644
--- a/app/Notifications/NewActivityNotification.php
+++ b/app/Notifications/NewActivityNotification.php
@@ -30,7 +30,7 @@ class NewActivityNotification extends Notification
*/
public function via(object $notifiable): array
{
- return [WebPushChannel::class];
+ return ['database', WebPushChannel::class];
}
/**
@@ -46,15 +46,6 @@ class NewActivityNotification extends Notification
->action('View', 'view')
->data(['url' => $this->activity['url']]);
- switch (get_class($this->activity)) {
- case Message::class:
- /** @var Chat $chat */
- $chat = $this->activity->chat;
-
- $result->data(['url' => $chat->subjectUrl]);
- break;
- }
-
return $result;
}
@@ -66,7 +57,8 @@ class NewActivityNotification extends Notification
public function toArray(object $notifiable): array
{
return [
- //
+ 'content' => $this->activity['content'],
+ 'url' => $this->activity['url'],
];
}
}
diff --git a/database/migrations/2026_07_06_100015_create_notifications_table.php b/database/migrations/2026_07_06_100015_create_notifications_table.php
new file mode 100644
index 0000000..d738032
--- /dev/null
+++ b/database/migrations/2026_07_06_100015_create_notifications_table.php
@@ -0,0 +1,31 @@
+uuid('id')->primary();
+ $table->string('type');
+ $table->morphs('notifiable');
+ $table->text('data');
+ $table->timestamp('read_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('notifications');
+ }
+};
diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml
new file mode 100644
index 0000000..24fbe45
--- /dev/null
+++ b/phpunit.dusk.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ ./tests/Browser
+
+
+
diff --git a/phpunit.xml b/phpunit.xml
index e7f0a48..34af592 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -23,7 +23,7 @@
-
+
diff --git a/resources/js/components/AddMutationForm.vue b/resources/js/components/AddMutationForm.vue
index b28af2b..2d522d6 100644
--- a/resources/js/components/AddMutationForm.vue
+++ b/resources/js/components/AddMutationForm.vue
@@ -104,6 +104,7 @@ function submit() {
id="amount"
type="number"
class="c-add-mutation-form__input"
+ data-test="amount-input"
/>
Add Mutation
diff --git a/tests/Browser/NotificationTest.php b/tests/Browser/NotificationTest.php
new file mode 100644
index 0000000..b757337
--- /dev/null
+++ b/tests/Browser/NotificationTest.php
@@ -0,0 +1,78 @@
+artisan('migrate:fresh', ['--seed' => true]);
+ }
+
+ /**
+ * A basic browser test example.
+ */
+ public function test_it_shows_notifications_on_mutation_activity(): void
+ {
+ $owner = User::factory()->create();
+ $participant = User::factory()->create();
+ $dynamic = Dynamic::factory()->create();
+ $dynamic->chat()->create();
+ $dynamic->participants()->attach($owner->id, ['role' => 'owner']);
+ $dynamic->participants()->attach($participant->id, ['role' => 'participant']);
+ $ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
+
+ $this->browse(function (Browser $ownerBrowser, Browser $participantBrowser) use ($owner, $participant, $dynamic, $ledger) {
+ $ownerBrowser->loginAs($owner)
+ ->visit(route('dynamics.ledgers.show', [$dynamic, $ledger]))
+ ->waitForText($ledger->name)
+ ->assertSee($ledger->name)
+ ->script([
+ "window.notifications = [];",
+ "window.Notification = function(title, options) { window.notifications.push({title, options}); };",
+ ]);
+
+ $participantBrowser->loginAs($participant)
+ ->visit(route('dynamics.ledgers.show', [$dynamic, $ledger]))
+ ->waitForText('Add Mutation')
+ ->type('[data-test="description-input"]', 'A new task suggestion')
+ ->type('[data-test="amount-input"]', 10)
+ ->press('[data-test="add-mutation-button"]')
+ ->waitForText('A new task suggestion');
+
+ $ownerBrowser->pause(1000);
+ $lastNotification = $ownerBrowser->script("return window.notifications.pop();")[0] ?? null;
+ $this->assertNotNull($lastNotification, "Owner did not receive the 'new suggestion' notification.");
+ $this->assertEquals('New Activity', $lastNotification['title']);
+ $this->assertStringContainsString('suggested a new entry', $lastNotification['options']['body']);
+
+ $mutation = Mutation::where('description', 'A new task suggestion')->first();
+ $ownerBrowser->press("#mutation-{$mutation->id}-approve")
+ ->waitForText('APPROVED');
+
+ $participantBrowser->script([
+ "window.notifications = [];",
+ "window.Notification = function(title, options) { window.notifications.push({title, options}); };",
+ ]);
+
+ $ownerBrowser->pause(1000);
+
+ $participantBrowser->pause(1000);
+ $lastNotification = $participantBrowser->script("return window.notifications.pop();")[0] ?? null;
+ $this->assertNotNull($lastNotification, "Participant did not receive the 'approved' notification.");
+ $this->assertEquals('New Activity', $lastNotification['title']);
+ $this->assertStringContainsString('APPROVED the suggestion', $lastNotification['options']['body']);
+ });
+ }
+}
diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php
index b1ff66e..f84ae30 100644
--- a/tests/DuskTestCase.php
+++ b/tests/DuskTestCase.php
@@ -31,7 +31,7 @@ abstract class DuskTestCase extends BaseTestCase
$options = (new ChromeOptions)->addArguments((new Collection([
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
'--disable-gpu',
- '--headless=new',
+// '--headless=new',
'--no-sandbox',
'--disable-dev-shm-usage',
]))->unless(static::runningInSail(), function (Collection $arguments) {