TenantAtlas/tests/Unit/Support/Diff/DiffPresenterTest.php
ahmido 0b5cadc234 feat: add shared diff presentation foundation (#170)
## 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
2026-03-14 12:32:08 +00:00

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.');
});