Files
amare/tests/Feature/Filament/AdminPanelBrandParityTest.php
Manoel Freitas 63c0269489 feat: aplicar identidade Heritage Editorial ao painel Filament (MAN-118) (#40)
* feat: aplicar identidade Heritage Editorial ao painel Filament

O painel admin ainda usava os defaults de fábrica do Filament (Amber,
Zinc, Inter Variable, dark mode ligado) enquanto o site público já
seguia o design system Heritage Editorial há várias entregas — a
inconsistência ficava evidente para quem navegava entre as duas áreas
e não havia nenhum teste travando a configuração do painel.

- Cores primary/gray/danger/warning/success viram arrays explícitos de
  11 tons (não Color::hex()/string), a única forma de preservar os
  hexadecimais exatos do DESIGN.md — Color::hex() decompõe a cor e
  remonta lightness/chroma por uma tabela fixa, perdendo a cor real.
  Os tons foram escolhidos verificando empiricamente com
  ButtonComponentColorMap qual shade o botão sólido realmente usa
  (600/500/50), e o tom 50 do primary é um verde-oliva claro (não
  branco puro) porque BadgeComponent usa bg-color-50 diretamente e um
  branco puro tornaria o badge quase invisível sobre o Papel Marfim.
- EB Garamond self-hosted via LocalFontProvider (sans/mono/serif), sem
  reintroduzir uma requisição externa ao fonts.bunny.net — o hook
  HEAD_END reaproveita o mesmo <x-fonts /> do site público.
- Novo tema Vite (resources/css/filament/admin/theme.css) zera radius
  e shadow só no escopo do Filament, incluindo a variável --radius
  "bare" (usada em ~227 regras do próprio Filament) e um override
  manual para o CSS pré-compilado do tooltip Tippy.js, que não é
  alcançado pelo @theme.
- Dark mode desativado: o Heritage Editorial é uma paleta única.
- Teste de regressão (AdminPanelBrandParityTest) pinando cores, fontes
  e configuração do tema, incluindo uma renderização real de
  /admin/login — foi essa renderização que pegou um bug real: passar
  a família já entre aspas simples quebrava a declaração CSS
  (--font-family: ''EB Garamond''), silenciosamente caindo para
  ui-sans-serif.

Bundle público verificado byte a byte: nenhuma declaração CSS
existente mudou de valor (apenas classes novas e não usadas pelo site
público foram adicionadas ao app.css, um efeito colateral inerte de
ter uma segunda entrada Tailwind no mesmo build do Vite).

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* fix(build): copiar CSS do Filament no estágio frontend e neutralizar sombras

Dois defeitos encontrados ao revisar a paridade visual do painel.

O primeiro impedia o build da imagem. O `resources/css/filament/admin/
theme.css` importa o CSS não compilado do próprio Filament, e o estágio
`frontend` do Dockerfile copia apenas `package.json`, `vite.config.js`,
`resources` e `public` — nunca `vendor`. O `npm run build` falhava na
resolução do import, o que derrubaria os jobs `container` e `browser` do
CI. Agora a subárvore `vendor/filament` é copiada do estágio do composer,
em vez de todo o `vendor`, para manter o contexto pequeno.

O segundo é silencioso e mais interessante. O plugin do Tailwind compartilha
um único contexto entre todas as entradas do build, então o `@source
app/Filament/**` da entrada do painel torna visível o uso de `shadow-*`
do Filament e o Tailwind passa a emitir seus valores padrão de
`--shadow-sm/md/lg` também no bundle público. Verificado por diff do
`app.css` construído com e sem a entrada do tema.

Nada no site público usa utilitário de sombra hoje, então a renderização
não muda e os baselines visuais seguem válidos. Mas deixar valores reais
de sombra definidos no CSS que vai para produção permitiria que um
`shadow-sm` futuro em elemento público violasse a Tonal Layer Rule do
DESIGN.md em silêncio — e o HeritageEditorialTokensTest só inspeciona
arquivos de origem, então não pegaria.

Os tokens de sombra passam a ser fixados em transparente no `@theme` do
`app.css`, com teste que falha se algum deixar de ser. A regra continua
verdadeira no artefato que realmente ship.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 11:09:01 -03:00

197 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Filament;
use App\Providers\Filament\AdminPanelProvider;
use Filament\Facades\Filament;
use Filament\FontProviders\LocalFontProvider;
use Filament\Support\Colors\Color;
use ReflectionMethod;
use Tests\TestCase;
/**
* Pins the Heritage Editorial parity applied to the admin panel (MAN-118)
* the same way HeritageEditorialTokensTest pins the public site's tokens, so
* a future edit to AdminPanelProvider/the Filament theme entry cannot
* silently drift back toward Filament's stock Amber/Zinc/dark-mode defaults.
*/
class AdminPanelBrandParityTest extends TestCase
{
/**
* @return array<int, string>
*/
private function invokeColorRamp(string $method): array
{
$reflection = new ReflectionMethod(AdminPanelProvider::class, $method);
$reflection->setAccessible(true);
return $reflection->invoke(new AdminPanelProvider($this->app));
}
public function test_primary_ramp_pins_oliva_heranca_and_oliva_profundo(): void
{
$primary = $this->invokeColorRamp('primaryColor');
$this->assertSame('#556B2F', $primary[600]);
$this->assertSame('#3E5219', $primary[700]);
$this->assertSame('#8B9D77', $primary[300]);
// Deliberately NOT pure white: BadgeComponent/badge.css apply
// `bg-color-50` directly as a badge's background, and a pure-white
// 50 would render a "primary" badge as an invisible pill against
// the Papel Marfim (#FBF9F4) panel body.
$this->assertSame('#F3F5EC', $primary[50]);
$this->assertNotSame('#FFFFFF', $primary[50]);
}
public function test_gray_ramp_pins_the_paper_and_ink_tokens(): void
{
$gray = $this->invokeColorRamp('grayColor');
$this->assertSame('#FBF9F4', $gray[50]);
$this->assertSame('#F0EEE9', $gray[100]);
$this->assertSame('#E4E2DD', $gray[200]);
$this->assertSame('#C5C8B8', $gray[300]);
$this->assertSame('#5D6155', $gray[500]);
$this->assertSame('#1B1C19', $gray[900]);
}
public function test_semantic_ramps_pin_the_existing_amare_hexes_at_shade_600(): void
{
$this->assertSame('#991B1B', $this->invokeColorRamp('dangerColor')[600]);
$this->assertSame('#92400E', $this->invokeColorRamp('warningColor')[600]);
$this->assertSame('#166534', $this->invokeColorRamp('successColor')[600]);
}
public function test_oliva_heranca_and_tinta_oliva_pairs_stay_wcag_aa(): void
{
$primary = $this->invokeColorRamp('primaryColor');
$gray = $this->invokeColorRamp('grayColor');
// White CTA text on the Oliva Herança solid-button background.
$this->assertGreaterThanOrEqual(
Color::WCAG_AA_TEXT,
Color::calculateContrastRatio($primary[600], '#FFFFFF'),
);
// Tinta Oliva body copy on the Papel Marfim panel background.
$this->assertGreaterThanOrEqual(
Color::WCAG_AA_TEXT,
Color::calculateContrastRatio($gray[900], $gray[50]),
);
// Badge text (BadgeComponent resolves this to shade 500 for this
// ramp) against the badge's own bg-color-50 surface.
$this->assertGreaterThanOrEqual(
Color::WCAG_AA_TEXT,
Color::calculateContrastRatio($primary[50], $primary[500]),
);
}
public function test_admin_panel_disables_dark_mode(): void
{
$this->assertFalse(Filament::getPanel('admin')->hasDarkMode());
}
public function test_admin_panel_uses_the_amare_brand_name(): void
{
$this->assertSame('Amare Assessoria', Filament::getPanel('admin')->getBrandName());
}
public function test_admin_panel_self_hosts_eb_garamond_via_local_font_provider(): void
{
$panel = Filament::getPanel('admin');
// Passed bare, not pre-quoted: Filament's own base layout wraps
// getFontFamily() in single quotes itself
// (`--font-family: '{!! filament()->getFontFamily() !!}';`), so a
// value of "'EB Garamond'" here would render as the broken
// `--font-family: ''EB Garamond'';` and silently fail to parse as a
// font-family, falling back to ui-sans-serif — verified against the
// actual rendered /admin/login output below, not assumed.
$this->assertSame('EB Garamond', $panel->getFontFamily());
$this->assertSame(LocalFontProvider::class, $panel->getFontProvider());
// font-mono / font-serif utilities (e.g. KeyValue in
// ManageSiteSettings) must not silently fall back to a system stack.
$this->assertSame('EB Garamond', $panel->getMonoFontFamily());
$this->assertSame(LocalFontProvider::class, $panel->getMonoFontProvider());
$this->assertSame('EB Garamond', $panel->getSerifFontFamily());
$this->assertSame(LocalFontProvider::class, $panel->getSerifFontProvider());
}
public function test_admin_login_page_renders_the_self_hosted_face_and_brand(): void
{
// The only assertion that actually exercises ->viteTheme(), the
// HEAD_END render hook, and ->brandLogo() end to end — the panel
// config getters above can't catch a broken manifest reference, a
// render hook that throws, or a brandLogo closure that fails outside
// a real request. Requires `npm run build` to have populated
// public/build (CI's `test` job does this before running tests).
$response = $this->get('/admin/login');
$response->assertOk();
// Filament's base layout emits `--font-family: '<value>';` — a
// single-quoted value, since it does the wrapping itself.
$response->assertSee("--font-family: 'EB Garamond';", escape: false);
$response->assertSee("--mono-font-family: 'EB Garamond';", escape: false);
$response->assertSee("--serif-font-family: 'EB Garamond';", escape: false);
$response->assertSee('brand/lockup-on-light.webp', escape: false);
// The actual requirement behind pinning LocalFontProvider: no
// external font request leaks into the panel's <head>.
$response->assertDontSee('fonts.bunny.net');
$response->assertDontSee('fonts.googleapis.com');
}
public function test_admin_panel_is_wired_to_its_own_vite_theme_entry(): void
{
$this->assertSame(
'resources/css/filament/admin/theme.css',
Filament::getPanel('admin')->getViteTheme(),
);
}
public function test_admin_theme_entry_zeroes_radius_and_shadow_tokens(): void
{
$theme = (string) file_get_contents(resource_path('css/filament/admin/theme.css'));
$this->assertStringContainsString(
"@import '../../../../vendor/filament/filament/resources/css/theme.css'",
$theme,
);
$this->assertStringContainsString('--radius: 0', $theme);
$this->assertStringContainsString('--radius-sm: 0', $theme);
$this->assertStringContainsString('--radius-md: 0', $theme);
$this->assertStringContainsString('--radius-lg: 0', $theme);
$this->assertStringContainsString('--radius-xl: 0', $theme);
$this->assertStringContainsString('--shadow: 0 0 #0000', $theme);
$this->assertStringContainsString('--shadow-sm: 0 0 #0000', $theme);
$this->assertStringContainsString('--shadow-md: 0 0 #0000', $theme);
$this->assertStringContainsString('--shadow-lg: 0 0 #0000', $theme);
// Manual override for the vendored Tippy.js tooltip CSS that ships
// already-compiled in vendor/filament/support/dist/index.css and
// therefore never sees the @theme overrides above.
$this->assertStringContainsString('.tippy-box', $theme);
}
public function test_vite_builds_the_admin_theme_entry(): void
{
$vite = (string) file_get_contents(base_path('vite.config.js'));
$this->assertStringContainsString("'resources/css/filament/admin/theme.css'", $vite);
}
public function test_admin_panel_brand_logo_reuses_the_public_lockup_asset(): void
{
$panel = Filament::getPanel('admin');
$this->assertSame(asset('brand/lockup-on-light.webp'), $panel->getBrandLogo());
$this->assertFileExists(base_path('public/brand/lockup-on-light.webp'));
}
}