## Summary - add a shared diff presentation layer under `app/Support/Diff` with deterministic row classification, summary derivation, and value stringification - centralize diff-state badge semantics through `BadgeCatalog` with a dedicated `DiffRowStatusBadge` - add reusable Filament diff partials, focused Pest coverage, and the full SpecKit artifact set for spec 141 ## Testing - `vendor/bin/sail artisan test --compact tests/Unit/Support/Diff/DiffRowStatusTest.php tests/Unit/Support/Diff/DiffRowTest.php tests/Unit/Support/Diff/DiffPresenterTest.php tests/Unit/Support/Diff/ValueStringifierTest.php tests/Unit/Badges/DiffRowStatusBadgeTest.php tests/Feature/Support/Diff/SharedDiffSummaryPartialTest.php tests/Feature/Support/Diff/SharedDiffRowPartialTest.php tests/Feature/Support/Diff/SharedInlineListDiffPartialTest.php` - `vendor/bin/sail bin pint --dirty --format agent` ## Filament / Livewire Contract - Livewire v4.0+ compliance: unchanged and respected; this feature adds presentation support only within the existing Filament v5 / Livewire v4 stack - Provider registration: unchanged; no panel/provider changes were required, so `bootstrap/providers.php` remains the correct registration location - Global search: unchanged; no Resource or global-search behavior was added or modified - Destructive actions: none introduced in this feature - Asset strategy: no new registered Filament assets; shared Blade partials rely on the existing asset pipeline and standard deploy step for `php artisan filament:assets` when assets change generally - Testing coverage: presenter, DTOs, stringifier, badge semantics, summary partial, row partial, and inline-list partial are covered by focused Pest unit and feature tests ## Notes - Spec checklist status is complete for `specs/141-shared-diff-presentation-foundation/checklists/requirements.md` - This PR preserves specialized diff renderers and documents incremental adoption rather than forcing migration in the same change Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #170
128 lines
4.2 KiB
PHP
128 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Support\Diff\DiffPresenter;
|
|
use App\Support\Diff\DiffRowStatus;
|
|
|
|
it('classifies rows across all shared states and derives matching summary counts', function (): void {
|
|
$presentation = app(DiffPresenter::class)->present(
|
|
baseline: [
|
|
'description' => 'Before',
|
|
'display_name' => 'TenantPilot',
|
|
'retired_setting' => 'Legacy',
|
|
],
|
|
current: [
|
|
'description' => 'After',
|
|
'display_name' => 'TenantPilot',
|
|
'new_setting' => 'Enabled',
|
|
],
|
|
labels: [
|
|
'description' => 'Description',
|
|
'display_name' => 'Display name',
|
|
'new_setting' => 'New setting',
|
|
'retired_setting' => 'Retired setting',
|
|
],
|
|
);
|
|
|
|
$rows = collect($presentation->rows)->keyBy('key');
|
|
|
|
expect($presentation->summary->changedCount)->toBe(1)
|
|
->and($presentation->summary->addedCount)->toBe(1)
|
|
->and($presentation->summary->removedCount)->toBe(1)
|
|
->and($presentation->summary->unchangedCount)->toBe(1)
|
|
->and($presentation->summary->hasRows)->toBeTrue()
|
|
->and($rows->get('description')?->status)->toBe(DiffRowStatus::Changed)
|
|
->and($rows->get('display_name')?->status)->toBe(DiffRowStatus::Unchanged)
|
|
->and($rows->get('new_setting')?->status)->toBe(DiffRowStatus::Added)
|
|
->and($rows->get('retired_setting')?->status)->toBe(DiffRowStatus::Removed);
|
|
});
|
|
|
|
it('returns rows in deterministic label order', function (): void {
|
|
$presentation = app(DiffPresenter::class)->present(
|
|
baseline: [
|
|
'zeta_value' => 'same',
|
|
'alpha_value' => 'before',
|
|
],
|
|
current: [
|
|
'alpha_value' => 'after',
|
|
'middle_value' => 'new',
|
|
'zeta_value' => 'same',
|
|
],
|
|
labels: [
|
|
'alpha_value' => 'Alpha value',
|
|
'middle_value' => 'Middle value',
|
|
'zeta_value' => 'Zeta value',
|
|
],
|
|
);
|
|
|
|
expect(array_map(
|
|
static fn ($row): string => $row->label,
|
|
$presentation->rows,
|
|
))->toBe([
|
|
'Alpha value',
|
|
'Middle value',
|
|
'Zeta value',
|
|
]);
|
|
});
|
|
|
|
it('prepares inline list fragments for simple list comparisons', function (): void {
|
|
$presentation = app(DiffPresenter::class)->present(
|
|
baseline: [
|
|
'scope_tags' => ['Default', 'Legacy', 'Shared'],
|
|
],
|
|
current: [
|
|
'scope_tags' => ['Default', 'Shared', 'Workspace'],
|
|
],
|
|
labels: [
|
|
'scope_tags' => 'Scope tags',
|
|
],
|
|
);
|
|
|
|
$row = $presentation->rows[0] ?? null;
|
|
|
|
expect($row)->not->toBeNull()
|
|
->and($row?->isListLike)->toBeTrue()
|
|
->and($row?->addedItems)->toBe(['Workspace'])
|
|
->and($row?->removedItems)->toBe(['Legacy'])
|
|
->and($row?->unchangedItems)->toBe(['Default', 'Shared']);
|
|
});
|
|
|
|
it('falls back to generated labels and safe empty meta when optional metadata is missing or invalid', function (): void {
|
|
$presentation = app(DiffPresenter::class)->present(
|
|
baseline: [
|
|
'customSettingFoo' => 'Before',
|
|
],
|
|
current: [
|
|
'customSettingFoo' => 'After',
|
|
],
|
|
meta: [
|
|
'customSettingFoo' => 'invalid-meta-shape',
|
|
],
|
|
);
|
|
|
|
$row = $presentation->rows[0] ?? null;
|
|
|
|
expect($row)->not->toBeNull()
|
|
->and($row?->label)->toBe('Custom Setting Foo')
|
|
->and($row?->meta)->toBe([]);
|
|
});
|
|
|
|
it('returns a no-data presentation for empty or sparse compare payloads', function (): void {
|
|
$presentation = app(DiffPresenter::class)->present(
|
|
baseline: [],
|
|
current: [],
|
|
changedKeys: ['ghost_key'],
|
|
labels: ['ghost_key' => 'Ghost key'],
|
|
meta: ['ghost_key' => ['note' => 'unused']],
|
|
);
|
|
|
|
expect($presentation->rows)->toBe([])
|
|
->and($presentation->summary->hasRows)->toBeFalse()
|
|
->and($presentation->summary->changedCount)->toBe(0)
|
|
->and($presentation->summary->addedCount)->toBe(0)
|
|
->and($presentation->summary->removedCount)->toBe(0)
|
|
->and($presentation->summary->unchangedCount)->toBe(0)
|
|
->and($presentation->summary->message)->toBe('No diff data available.');
|
|
});
|