Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
196 lines
6.2 KiB
PHP
196 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Panel;
|
|
|
|
use App\Models\Page;
|
|
use App\Models\Project;
|
|
use App\Support\Health\Finding;
|
|
use App\Support\Health\HealthReport;
|
|
use App\Support\TranslationFreshness;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Translations the original has moved on without.
|
|
*
|
|
* The fault this catches is the one nothing else can see: the text is there in
|
|
* every language, so every screen calls the record finished, and one of those
|
|
* languages has been saying something the source stopped saying. It is worked
|
|
* out from the change history, so what these tests really hold still is that
|
|
* the history is being read the way it is written.
|
|
*/
|
|
class StaleTranslationsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function translated(): Page
|
|
{
|
|
return Page::factory()->published()->create([
|
|
'title' => ['es' => 'Uno', 'en' => 'One', 'ca' => 'U'],
|
|
'slug' => ['es' => 'uno', 'en' => 'one', 'ca' => 'u'],
|
|
'body' => ['es' => 'Texto', 'en' => 'Text', 'ca' => 'Text'],
|
|
]);
|
|
}
|
|
|
|
#[Test]
|
|
public function a_record_nobody_has_edited_is_not_out_of_date(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
// Written in three languages at once: whatever else is true of it,
|
|
// none of them is behind another.
|
|
$this->assertSame([], TranslationFreshness::for($page));
|
|
}
|
|
|
|
#[Test]
|
|
public function editing_the_original_leaves_the_other_languages_behind(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('title', 'es', 'Uno corregido')->save();
|
|
|
|
$stale = TranslationFreshness::for($page->fresh());
|
|
|
|
$this->assertSame(['en', 'ca'], array_keys($stale));
|
|
|
|
// And it says since when, which is what decides how urgent it is.
|
|
$this->assertTrue($stale['ca']->isToday());
|
|
}
|
|
|
|
#[Test]
|
|
public function catching_a_translation_up_clears_it(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('body', 'es', 'Texto nuevo')->save();
|
|
|
|
$this->travel(1)->days();
|
|
$page->fresh()->setTranslation('body', 'ca', 'Text nou')->save();
|
|
|
|
$stale = TranslationFreshness::for($page->fresh());
|
|
|
|
// Catalan caught up; English never did.
|
|
$this->assertArrayNotHasKey('ca', $stale);
|
|
$this->assertArrayHasKey('en', $stale);
|
|
}
|
|
|
|
/**
|
|
* A language with nothing in it is not out of date, it is untranslated —
|
|
* and there is already a check that says so. Reporting both about one
|
|
* record would just make two lists that never empty.
|
|
*/
|
|
#[Test]
|
|
public function a_missing_translation_is_not_a_stale_one(): void
|
|
{
|
|
$page = Page::factory()->published()->create([
|
|
'title' => ['es' => 'Uno'],
|
|
'slug' => ['es' => 'uno'],
|
|
]);
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('title', 'es', 'Uno corregido')->save();
|
|
|
|
$this->assertSame([], TranslationFreshness::for($page->fresh()));
|
|
}
|
|
|
|
/**
|
|
* Renaming the Spanish address does not make the Catalan text less true,
|
|
* and slugs are the one translatable field that routinely changes on their
|
|
* own — counting them would mark half the site out of date the first time
|
|
* somebody tidied a URL.
|
|
*/
|
|
#[Test]
|
|
public function renaming_a_slug_does_not_make_a_translation_stale(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('slug', 'es', 'uno-mejor')->save();
|
|
|
|
$this->assertSame([], TranslationFreshness::for($page->fresh()));
|
|
}
|
|
|
|
#[Test]
|
|
public function editing_a_translation_does_not_make_the_original_stale(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('title', 'ca', 'U corregit')->save();
|
|
|
|
// The source language is never reported against itself, and touching
|
|
// Catalan says nothing about English.
|
|
$this->assertSame([], TranslationFreshness::for($page->fresh()));
|
|
}
|
|
|
|
#[Test]
|
|
public function the_health_check_reports_it(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('body', 'es', 'Texto corregido')->save();
|
|
|
|
$finding = app(HealthReport::class)->runAll()->firstWhere('key', 'stale-translations');
|
|
|
|
$this->assertInstanceOf(Finding::class, $finding);
|
|
$this->assertSame(1, $finding->count);
|
|
$this->assertStringContainsString('ca', $finding->samples[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_works_for_every_translated_type(): void
|
|
{
|
|
$project = Project::factory()->published()->create([
|
|
'title' => ['es' => 'Proyecto', 'ca' => 'Projecte'],
|
|
'slug' => ['es' => 'proyecto', 'ca' => 'projecte'],
|
|
]);
|
|
|
|
$this->travel(1)->days();
|
|
$project->setTranslation('title', 'es', 'Proyecto nuevo')->save();
|
|
|
|
$this->assertArrayHasKey('ca', TranslationFreshness::for($project->fresh()));
|
|
}
|
|
|
|
/**
|
|
* The filter's half: the answer cannot be a SQL condition, so it is worked
|
|
* out per record and handed to the query as a list.
|
|
*/
|
|
#[Test]
|
|
public function the_panel_can_ask_for_the_ones_that_fell_behind(): void
|
|
{
|
|
$behind = $this->translated();
|
|
$fine = $this->translated();
|
|
|
|
$this->travel(1)->days();
|
|
$behind->setTranslation('title', 'es', 'Uno corregido')->save();
|
|
|
|
$ids = TranslationFreshness::staleIds(Page::query());
|
|
|
|
$this->assertContains($behind->id, $ids);
|
|
$this->assertNotContains($fine->id, $ids);
|
|
}
|
|
|
|
/**
|
|
* The answer is cached against the record's `updated_at`, so it has to be
|
|
* recomputed the moment anything about that record changes.
|
|
*/
|
|
#[Test]
|
|
public function the_cached_answer_does_not_survive_the_next_edit(): void
|
|
{
|
|
$page = $this->translated();
|
|
|
|
$this->assertSame([], TranslationFreshness::for($page));
|
|
|
|
$this->travel(1)->days();
|
|
$page->setTranslation('title', 'es', 'Uno corregido')->save();
|
|
|
|
$this->assertNotSame([], TranslationFreshness::for($page->fresh()));
|
|
}
|
|
}
|