Compare commits

..

2 Commits

Author SHA1 Message Date
Daan Meijer
b2a7e2557e dashboard gereparerd
Some checks failed
linter / quality (push) Failing after 1m5s
tests / ci (8.3) (push) Failing after 51s
tests / ci (8.4) (push) Failing after 1m6s
tests / ci (8.5) (push) Failing after 1m7s
2026-07-06 15:53:30 +02:00
Daan Meijer
ccb3598da0 notificationtest in progress 2026-07-06 13:27:52 +02:00
11 changed files with 210 additions and 24 deletions

View File

@ -11,10 +11,10 @@ class DashboardController extends Controller
public function index(Request $request, ActivityService $activityService)
{
$user = $request->user();
$unreadEntities = $activityService->getUnreadEntitiesGrouped($user);
$unreadDynamics = $activityService->getUnreadEntitiesGrouped($user);
return Inertia::render('Dashboard', [
'unreadEntities' => $unreadEntities,
'unreadDynamics' => $unreadDynamics,
]);
}
}

View File

@ -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'],
];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Policies;
use App\Models\Chat;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ChatPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Chat $chat): bool
{
$chatable = $chat->chatable;
return $user->can('view', $chatable);
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Chat $chat): bool
{
return $this->view($user, $chat);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Chat $chat): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Chat $chat): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Chat $chat): bool
{
return false;
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->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');
}
};

15
phpunit.dusk.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
beStrictAboutTestsThatDoNotTestAnything="false"
colors="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false">
<testsuites>
<testsuite name="Browser Test Suite">
<directory suffix="Test.php">./tests/Browser</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -23,7 +23,7 @@
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_CONNECTION" value="sqlite" />
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/>

View File

@ -104,6 +104,7 @@ function submit() {
id="amount"
type="number"
class="c-add-mutation-form__input"
data-test="amount-input"
/>
<div
v-if="form.errors.amount"
@ -122,6 +123,7 @@ function submit() {
id="description"
rows="4"
class="c-add-mutation-form__textarea"
data-test="description-input"
></textarea>
<div
v-if="form.errors.description"
@ -171,6 +173,7 @@ function submit() {
type="submit"
:disabled="form.processing"
class="c-add-mutation-form__submit-btn"
data-test="add-mutation-button"
>
Add Mutation
</button>

View File

@ -14,7 +14,7 @@ defineOptions({
});
defineProps<{
unreadEntities: Array<{
unreadDynamics: Array<{
id: number;
name: string;
url: string;
@ -50,15 +50,17 @@ function formatTime(isoString: string): string {
<div class="c-dashboard__container">
<h2 class="c-dashboard__title">Recent Activity</h2>
<div v-if="unreadEntities.length > 0" class="c-dashboard__grid">
<div v-if="unreadDynamics.length > 0" class="c-dashboard__grid">
<div
v-for="entity in unreadEntities"
:key="entity.id"
v-for="dynamic in unreadDynamics"
:key="dynamic.id"
class="c-dashboard__card"
>
<div class="c-dashboard__card-header">
<div class="c-dashboard__entity-meta">
<span class="c-dashboard__badge-type c-dashboard__badge-type--dynamic">
<span
class="c-dashboard__badge-type c-dashboard__badge-type--dynamic"
>
Dynamic
</span>
<span class="c-dashboard__unread-count">
@ -102,9 +104,7 @@ function formatTime(isoString: string): string {
v-if="dynamic.new_activities.length > 0"
class="c-dashboard__divider"
>
<span class="c-dashboard__divider-text"
>NEW</span
>
<span class="c-dashboard__divider-text">NEW</span>
</div>
<!-- New / Unread Activities -->

View File

@ -8,5 +8,5 @@ Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
});
Broadcast::channel('chats.{chat}', function ($user, Chat $chat) {
return $user->can('view', $chat->chatable);
return $user?->can('view', $chat);
});

View File

@ -0,0 +1,78 @@
<?php
namespace Tests\Browser;
use App\Models\Dynamic;
use App\Models\Ledger;
use App\Models\Mutation;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class NotificationTest extends DuskTestCase
{
use DatabaseMigrations;
protected function setUp(): void
{
parent::setUp();
$this->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']);
});
}
}

View File

@ -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) {