Le piantine non dipendono piu' da un territorio esistente: territorio_id e' nullable, la chiave esterna e' nullOnDelete invece di cascadeOnDelete, e `nome` fa da etichetta finche' il collegamento manca. Si disegna guardando le strade e si decide dopo di chi e' il disegno; se il territorio non c'e', lo si crea dall'editor con numero, zona, tipologia e confini — zona e tipologia comprese, create sul posto quando mancano. Il cambio di cascata chiude anche una perdita di dati silenziosa: ogni import XML svuota `territori` in blocco e finora si portava via tutte le piantine. Il disegno passa su /piantine/editor, a schermo intero con telaio proprio: su una mappa larga un terzo di schermo un confine si sbaglia, e finisce stampato. /piantine resta la gestione — area, generazione, collegamenti, storico. Cartolina S-12 reimpaginata: pagina 1 il modulo con la Localita' (che ora e' il nome della zona, non un campo duplicato) e i confini scritti nello spazio centrale, con il corpo del testo che si adatta alla lunghezza; pagina 2 la piantina sola al 98%. Aggiunta l'anteprima, che compone il PDF al volo senza toccare pdf_path ne' lo storico. Immagine: JPEG invece di PNG (0,9 MB contro 5 per territorio), composta alle tessere dello zoom piu' alto e ridotta con Lanczos, nelle proporzioni della pagina cosi' riempie il foglio invece di lasciare fasce bianche. Il tratto del confine e' espresso in pixel dell'immagine finale: si vede il confine e si legge la via che ci passa sotto. L'attribuzione OSM si dimensiona con l'immagine, come la ODbL richiede. Rimossa la rigenerazione massiva: job, stato in cache e interfaccia. Sistema visivo ripassato su tutta l'applicazione: le scale gray e indigo di Tailwind sono ridefinite nei token, cosi' le 37 viste si riallineano insieme invece di essere riscritte una per una. Grigi caldi, un accento solo con quattro significati, carattere servito dall'applicazione e non da Google Fonts, via il gradiente della testata. Aggiunto il link "salta al contenuto", che mancava. Corretto un guasto d'ambiente che bloccava TUTTI gli upload in sviluppo: dalla 1.26 il fastcgi_params di nginx passa HTTP_HOST come $host, che perde la porta, e ogni URL firmato smetteva di validare — l'endpoint di upload di Livewire rispondeva 401. 294 test verdi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
172 lines
5.9 KiB
PHP
172 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\AnnoTeocratico;
|
|
use App\Models\Assegnazione;
|
|
use App\Models\Campagna;
|
|
use App\Models\Proclamatore;
|
|
use App\Models\Setting;
|
|
use App\Models\Territorio;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use Spatie\Permission\Models\Role;
|
|
use Spatie\Permission\PermissionRegistrar;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Ogni pagina dell'applicazione si apre davvero.
|
|
*
|
|
* Nasce dall'unificazione dello stile, che ha toccato tutte le viste: un errore
|
|
* di markup non fallisce come un errore di codice — non rompe niente finche'
|
|
* qualcuno non apre quella pagina, e con venti viste modificate in una volta
|
|
* "qualcuno" e' un utente, non chi ha fatto la modifica.
|
|
*
|
|
* Il controllo su `<x-` non e' teorico: un tag di componente che il compilatore
|
|
* non riconosce resta nella pagina come testo. Non solleva, non rompe il
|
|
* layout in modo evidente — semplicemente il pulsante non c'e'.
|
|
*/
|
|
class PagineApplicativeTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Gli asset compilati non stanno nell'immagine di test: vedi la nota in
|
|
// PaginaBackupImportTest.
|
|
$this->withoutVite();
|
|
}
|
|
|
|
/**
|
|
* Rotte senza parametri. Quelle con un modello nell'URL hanno un test a
|
|
* parte, perche' hanno bisogno che il modello esista.
|
|
*
|
|
* @return array<string, array{string}>
|
|
*/
|
|
public static function rotteSempliciProvider(): array
|
|
{
|
|
$rotte = [
|
|
'dashboard',
|
|
'profile',
|
|
'territori.index',
|
|
'territori.create',
|
|
'territori.cestino',
|
|
'proclamatori.index',
|
|
'proclamatori.create',
|
|
'proclamatori.cestino',
|
|
'campagne.index',
|
|
'campagne.create',
|
|
'registro.index',
|
|
'audit.index',
|
|
'settings.edit',
|
|
'users.index',
|
|
'zone.index',
|
|
'tipologie.index',
|
|
'xml.exchange',
|
|
'piantine.index',
|
|
'piantine.editor',
|
|
'privacy',
|
|
'assegnazioni.assegna',
|
|
];
|
|
|
|
return array_combine(
|
|
$rotte,
|
|
array_map(fn (string $rotta) => [$rotta], $rotte)
|
|
);
|
|
}
|
|
|
|
#[DataProvider('rotteSempliciProvider')]
|
|
public function test_la_pagina_si_apre_senza_markup_rimasto_da_compilare(string $rotta): void
|
|
{
|
|
$risposta = $this->actingAs($this->admin())->get(route($rotta));
|
|
|
|
$risposta->assertOk();
|
|
|
|
$this->assertNessunTagDaCompilare($risposta->getContent(), $rotta);
|
|
}
|
|
|
|
public function test_le_pagine_legate_a_un_record_si_aprono(): void
|
|
{
|
|
$this->actingAs($this->admin());
|
|
|
|
$territorio = Territorio::create(['numero' => '1', 'attivo' => true]);
|
|
$proclamatore = Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true]);
|
|
$campagna = Campagna::create([
|
|
'descrizione' => 'Campagna di prova',
|
|
'start_date' => '2026-01-01',
|
|
'end_date' => '2026-12-31',
|
|
]);
|
|
|
|
$assegnazione = Assegnazione::create([
|
|
'territorio_id' => $territorio->id,
|
|
'proclamatore_id' => $proclamatore->id,
|
|
'anno_teocratico_id' => AnnoTeocratico::corrente()->id,
|
|
'assigned_at' => '2026-02-01',
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
$pagine = [
|
|
'territori.show' => route('territori.show', $territorio),
|
|
'territori.edit' => route('territori.edit', $territorio),
|
|
'proclamatori.show' => route('proclamatori.show', $proclamatore),
|
|
'proclamatori.edit' => route('proclamatori.edit', $proclamatore),
|
|
'campagne.show' => route('campagne.show', $campagna),
|
|
'campagne.edit' => route('campagne.edit', $campagna),
|
|
'assegnazioni.rientra' => route('assegnazioni.rientra', $assegnazione),
|
|
];
|
|
|
|
foreach ($pagine as $nome => $url) {
|
|
$risposta = $this->get($url);
|
|
|
|
$risposta->assertOk();
|
|
$this->assertNessunTagDaCompilare($risposta->getContent(), $nome);
|
|
}
|
|
}
|
|
|
|
public function test_la_schermata_di_cambio_password_si_apre(): void
|
|
{
|
|
$risposta = $this->actingAs($this->admin())->get(route('password.change'));
|
|
|
|
$risposta->assertOk();
|
|
$this->assertNessunTagDaCompilare($risposta->getContent(), 'password.change');
|
|
}
|
|
|
|
public function test_la_pagina_di_accesso_si_apre(): void
|
|
{
|
|
$risposta = $this->get(route('login'));
|
|
|
|
$risposta->assertOk();
|
|
$this->assertNessunTagDaCompilare($risposta->getContent(), 'login');
|
|
}
|
|
|
|
private function assertNessunTagDaCompilare(string $html, string $dove): void
|
|
{
|
|
$this->assertStringNotContainsString('<x-btn', $html, "{$dove}: tag <x-btn> rimasto da compilare.");
|
|
$this->assertStringNotContainsString('</x-btn>', $html, "{$dove}: tag </x-btn> rimasto da compilare.");
|
|
$this->assertStringNotContainsString('btn-action', $html, "{$dove}: e' rimasta una classe .btn-action, che non esiste piu' nel foglio di stile.");
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
Role::findOr('amministratore', fn () => Role::create(['name' => 'amministratore', 'guard_name' => 'web']));
|
|
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
|
|
|
// La testata legge le impostazioni: senza riga, il nome della
|
|
// congregazione ricade sul default e va bene lo stesso, ma le pagine
|
|
// che leggono le soglie vogliono trovarla.
|
|
Setting::firstOrCreate([], ['congregazione_nome' => 'Congregazione di prova']);
|
|
|
|
$utente = User::create([
|
|
'name' => 'Amministratore',
|
|
'email' => 'admin@example.test',
|
|
'password' => 'password-di-prova',
|
|
]);
|
|
$utente->assignRole('amministratore');
|
|
|
|
return $utente;
|
|
}
|
|
}
|