seed(RolePermissionSeeder::class); } #[Test] public function a_save_is_refused_when_the_record_changed_underneath_it(): void { $project = Project::factory()->create(['title' => ['es' => 'Original']]); $this->actingAs($this->userWithRoles('admin')); $editor = Livewire::test(EditProject::class, ['record' => $project->getKey()]); // Somebody else — a second tab, a teammate, does not matter which — // saves a change after this screen was opened. The truncated-second // precision of `updated_at` means a real difference needs distance in // time, not just a second call; a day is more than enough and mirrors // the wall-clock gap an actual second editor would produce. $this->travel(1)->day(); $project->fresh()->update(['title' => ['es' => 'Cambiado por otra persona']]); $editor ->fillForm(['title' => ['es' => 'Lo que yo escribía']]) ->call('save') ->assertNotified(__('panel.concurrency.title')); // Not "no form errors" — the save must not have reached the database // at all, and the other person's write must be exactly what survives. $this->assertSame('Cambiado por otra persona', $project->fresh()->getTranslation('title', 'es', false)); } #[Test] public function the_conflict_message_names_who_changed_it_when_the_history_knows(): void { $project = Project::factory()->create(['title' => ['es' => 'Original']]); $other = $this->userWithRoles('admin', ['name' => 'Ada Lovelace']); $this->actingAs($this->userWithRoles('admin')); $editor = Livewire::test(EditProject::class, ['record' => $project->getKey()]); $this->travel(1)->day(); $this->actingAs($other); $project->fresh()->update(['title' => ['es' => 'Cambiado']]); $editor ->fillForm(['title' => ['es' => 'Mi versión']]) ->call('save') ->assertNotified(__('panel.concurrency.title')); } #[Test] public function saving_without_interference_works_exactly_as_before(): void { $project = Project::factory()->create(['title' => ['es' => 'Original']]); $this->actingAs($this->userWithRoles('admin')); Livewire::test(EditProject::class, ['record' => $project->getKey()]) ->fillForm(['title' => ['es' => 'Actualizado']]) ->call('save') ->assertHasNoFormErrors(); $this->assertSame('Actualizado', $project->fresh()->getTranslation('title', 'es', false)); } /** * Saving twice from the same open screen — write, then write again — is * not a conflict with yourself. Each save has to become the new baseline, * or nobody could ever save more than once without reloading. */ #[Test] public function a_second_save_from_the_same_screen_is_not_a_conflict_with_itself(): void { $project = Project::factory()->create(['title' => ['es' => 'Original']]); $this->actingAs($this->userWithRoles('admin')); $editor = Livewire::test(EditProject::class, ['record' => $project->getKey()]) ->fillForm(['title' => ['es' => 'Primero']]) ->call('save') ->assertHasNoFormErrors(); $this->travel(1)->day(); $editor ->fillForm(['title' => ['es' => 'Segundo']]) ->call('save') ->assertHasNoFormErrors() ->assertNotNotified(__('panel.concurrency.title')); $this->assertSame('Segundo', $project->fresh()->getTranslation('title', 'es', false)); } /** * The guard is generic, not written against any one resource. Settings is * the one model with a string primary key rather than an * auto-incrementing id, which is exactly the kind of assumption a naive * `WHERE id = ?` would get wrong. */ #[Test] public function it_also_guards_a_model_with_a_string_primary_key(): void { Setting::set('site.maintenance', ['enabled' => false]); $this->actingAs($this->userWithRoles('super_admin')); $editor = Livewire::test(EditSettings::class, ['record' => 'site.maintenance']); $this->travel(1)->day(); Setting::set('site.maintenance', ['enabled' => true]); $editor ->fillForm(['value' => json_encode(['enabled' => false, 'changed_by_me' => true])]) ->call('save') ->assertNotified(__('panel.concurrency.title')); $this->assertSame(['enabled' => true], Setting::get('site.maintenance')); } #[Test] public function opening_a_record_someone_else_has_open_warns_the_second_person(): void { $project = Project::factory()->create(); $first = $this->userWithRoles('admin', ['name' => 'Ada Lovelace']); $this->actingAs($first); Livewire::test(EditProject::class, ['record' => $project->getKey()]); $this->actingAs($this->userWithRoles('admin')); Livewire::test(EditProject::class, ['record' => $project->getKey()]) ->assertNotified(__('panel.presence.title')); } /** * The first person to open a record has no reason to expect company, and * nothing this screen could tell them would help — so they are the one * case that stays silent. */ #[Test] public function the_first_person_to_open_a_record_is_not_warned(): void { $project = Project::factory()->create(); $this->actingAs($this->userWithRoles('admin')); Livewire::test(EditProject::class, ['record' => $project->getKey()]) ->assertNotNotified(__('panel.presence.title')); } #[Test] public function presence_fades_after_the_window_passes(): void { $project = Project::factory()->create(); $first = $this->userWithRoles('admin'); $this->actingAs($first); Livewire::test(EditProject::class, ['record' => $project->getKey()]); // Long past the window: a tab left open and forgotten must stop // haunting the record, not just for the display but for anyone else // who opens it afterwards. $this->travel(20)->minutes(); $this->actingAs($this->userWithRoles('admin')); Livewire::test(EditProject::class, ['record' => $project->getKey()]) ->assertNotNotified(__('panel.presence.title')); } #[Test] public function presence_never_counts_the_viewer_themselves(): void { $project = Project::factory()->create(); $user = $this->userWithRoles('admin'); $this->assertTrue(Presence::touch($project, $user)->isEmpty()); // Opening it again a moment later is still just one person here. $this->assertTrue(Presence::touch($project, $user)->isEmpty()); } }