update([ 'hero_title' => 'Celebrações com propósito', 'hero_image_path' => 'media/hero.jpg', 'hero_image_alt' => 'Capa editorial', ]); Service::factory()->published()->featured()->create(['title' => 'Casamentos']); PortfolioCase::factory()->published()->create([ 'title' => 'Caso A', 'is_featured' => true, ]); Testimonial::factory()->published()->create(['author_name' => 'Ana']); $response = $this->get(route('home')); $response ->assertOk() ->assertSee('data-motion="page-open"', false) ->assertSee('data-motion-beat="seal"', false) ->assertSee('data-motion-beat="title"', false) ->assertSee('data-motion-beat="media"', false) ->assertSee('data-motion-beat="cta"', false) ->assertSee('data-reveal-group', false) ->assertSee('data-reveal-from="up"', false) ->assertDontSee('data-chapter-index', false) ->assertDontSee('data-chapter-progress', false) ->assertSee('data-chapter="hero"', false) ->assertSee('data-chapter="sobre"', false) ->assertSee('data-reveal', false) ->assertDontSee('data-home-folio', false); } public function test_home_has_no_chapter_navigation_and_keeps_all_sections_when_cms_sections_are_empty(): void { SiteSetting::instance(); $response = $this->get(route('home')); $response ->assertOk() ->assertDontSee('data-chapter-index', false) ->assertDontSee('data-chapter-progress', false) ->assertSeeInOrder([ 'id="hero-heading"', 'id="manifesto-heading"', 'id="vertentes-heading"', 'id="packages-heading"', 'id="corporate-heading"', 'id="portfolio-heading"', 'id="testimonials-heading"', 'id="briefing-heading"', ], false); } public function test_motion_tokens_and_runtime_exist_with_reduced_motion_guards(): void { $tokens = (string) file_get_contents(resource_path('css/tokens.css')); $appCss = (string) file_get_contents(resource_path('css/app.css')); $appJs = (string) file_get_contents(resource_path('js/app.js')); $this->assertStringContainsString('--amare-motion-enter: 500ms', $tokens); $this->assertStringContainsString('--amare-motion-feedback: 150ms', $tokens); $this->assertStringContainsString('--amare-motion-stagger: 90ms', $tokens); $this->assertStringContainsString('--amare-motion-stagger-cap: 270ms', $tokens); $this->assertStringContainsString('--amare-motion-distance-y: 12px', $tokens); $this->assertStringContainsString('--amare-motion-distance-x: 24px', $tokens); $this->assertStringContainsString('--amare-motion-distance-x-mobile: 16px', $tokens); $this->assertStringContainsString('--amare-ease-arrival:', $tokens); $this->assertStringContainsString('--amare-motion-enter: 0.01ms', $tokens); $this->assertStringContainsString('[data-motion="page-open"]', $appCss); $this->assertStringContainsString('[data-motion-beat]', $appCss); $this->assertStringContainsString('[data-reveal-group]', $appCss); $this->assertStringContainsString('[data-reveal-from="left"]', $appCss); $this->assertStringContainsString('[data-reveal-from="right"]', $appCss); $this->assertStringContainsString('[data-reveal]', $appCss); $this->assertStringNotContainsString('[data-chapter-index]', $appCss); $this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss); // Entrance motion must not fade text opacity — mid-fade fails WCAG contrast (axe). $this->assertDoesNotMatchRegularExpression( '/\[data-motion-beat\][^{]*\{[^}]*opacity:\s*0/s', $appCss, ); $this->assertDoesNotMatchRegularExpression( '/\[data-motion="page-open"\]:not\(\.is-active\)[^{]*\{[^}]*opacity:\s*0/s', $appCss, ); $this->assertDoesNotMatchRegularExpression( '/\[data-reveal\]:not\(\.is-revealed\)[^{]*\{[^}]*opacity:\s*0/s', $appCss, ); $this->assertFileExists(resource_path('js/motion.js')); $this->assertStringContainsString("import './motion.js'", $appJs); $motionJs = (string) file_get_contents(resource_path('js/motion.js')); $this->assertStringNotContainsString('setupChapterIndex', $motionJs); $this->assertStringContainsString('prefers-reduced-motion', $motionJs); $this->assertStringContainsString("querySelectorAll('[data-reveal-group]')", $motionJs); $this->assertStringContainsString('Math.min(index, 3)', $motionJs); $this->assertStringContainsString('observer.unobserve(entry.target)', $motionJs); $this->assertStringContainsString("typeof IntersectionObserver !== 'function'", $motionJs); $this->assertStringContainsString('requestAnimationFrame', $motionJs); } public function test_all_visual_public_pages_expose_shared_opening_and_reveal_hooks(): void { SiteSetting::instance(); $case = PortfolioCase::factory()->published()->create([ 'slug' => 'casamento-jardim', 'title' => 'Casamento Jardim', ]); $responses = [ $this->get(route('home')), $this->get(route('services.index')), $this->get(route('portfolio.index')), $this->get(route('portfolio.show', $case->slug)), $this->get(route('about')), $this->get(route('contact')), $this->get(route('privacy')), ]; foreach ($responses as $response) { $response ->assertOk() ->assertSee('data-motion="page-open"', false) ->assertSee('data-motion-beat', false) ->assertSee('data-reveal-group', false) ->assertSee('data-reveal', false); } } public function test_technical_endpoints_do_not_expose_motion_contract(): void { SiteSetting::instance(); $this->get(route('sitemap')) ->assertOk() ->assertDontSee('data-motion=', false) ->assertDontSee('data-reveal', false); $this->get(route('robots')) ->assertOk() ->assertDontSee('data-motion=', false) ->assertDontSee('data-reveal', false); } }