update([ 'hero_title' => 'Celebrações com propósito', 'default_og_image_path' => 'media/hero.jpg', 'default_og_image_alt' => 'Capa editorial', ]); Service::factory()->published()->featured()->create([ 'title' => 'Casamentos', 'summary' => 'Assessoria completa', ]); }); it('keeps every home motion target in final visible state when reduced motion is preferred', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ]); $state = $page->script(<<<'JS' () => { const hero = document.querySelector('[data-motion="page-open"]'); const title = document.querySelector('#hero-heading'); const index = document.querySelector('[data-chapter-index]'); const active = document.querySelector('[data-chapter-index] [aria-current="true"]'); const titleStyle = title ? getComputedStyle(title) : null; const finalReveals = [...document.querySelectorAll('[data-reveal]')] .every((node) => node.classList.contains('is-revealed')); return { heroActive: hero?.classList.contains('is-active') ?? false, titleOpacity: titleStyle ? Number.parseFloat(titleStyle.opacity) : 0, hasIndex: Boolean(index), hasActiveChapter: Boolean(active), finalReveals, enhancementEnabled: document.documentElement.hasAttribute('data-motion'), }; } JS); expect($state['heroActive'])->toBeTrue(); expect($state['titleOpacity'])->toBeGreaterThan(0.9); expect($state['hasIndex'])->toBeTrue(); expect($state['hasActiveChapter'])->toBeTrue(); expect($state['finalReveals'])->toBeTrue(); expect($state['enhancementEnabled'])->toBeFalse(); }); it('activates the home opening with 500ms beats and capped 90ms stagger', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'no-preference', ]); $page->script(<<<'JS' () => new Promise((resolve) => { const wait = () => { const hero = document.querySelector('[data-motion="page-open"]'); if (hero?.classList.contains('is-active')) { resolve(true); return; } requestAnimationFrame(wait); }; wait(); setTimeout(() => resolve(false), 2000); }) JS); $state = $page->script(<<<'JS' () => { const hero = document.querySelector('[data-motion="page-open"]'); const beatDurations = [...hero.querySelectorAll('[data-motion-beat]')] .map((beat) => Number.parseFloat(getComputedStyle(beat).transitionDuration)); const methodItems = [...document.querySelectorAll('[data-chapter="method"] [data-reveal]')]; return { active: hero?.classList.contains('is-active') ?? false, beatDurations, methodIndexes: methodItems.map((item) => item.style.getPropertyValue('--motion-index')), }; } JS); expect($state['active'])->toBeTrue(); expect($state['beatDurations'])->each->toBe(0.5); expect($state['methodIndexes'])->toBe(['0', '1', '2', '3', '3']); }); it('reveals a section once and keeps it final after returning in the scroll', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'no-preference', ]); $before = $page->script(<<<'JS' () => document.querySelector('#final-cta-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false JS); $page->script(<<<'JS' () => { document.querySelector('#final-cta-heading')?.scrollIntoView({ block: 'center' }); return true; } JS); $page->script(<<<'JS' () => new Promise((resolve) => { const target = document.querySelector('#final-cta-heading')?.closest('[data-reveal]'); const wait = () => { if (target?.classList.contains('is-revealed')) { resolve(true); return; } requestAnimationFrame(wait); }; wait(); }) JS); $page->script('() => window.scrollTo({ top: 0, behavior: "instant" })'); $after = $page->script(<<<'JS' () => document.querySelector('#final-cta-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false JS); expect($before)->toBeFalse(); expect($after)->toBeTrue(); }); it('preserves testimonial directions with reduced lateral distance on mobile', function (): void { Testimonial::factory()->published()->create([ 'quote' => 'Primeiro relato.', 'sort_order' => 10, ]); Testimonial::factory()->published()->create([ 'quote' => 'Segundo relato.', 'sort_order' => 20, ]); $page = $this->visit('/', [ 'reducedMotion' => 'no-preference', ]); $readDistances = <<<'JS' () => [...document.querySelectorAll('[data-chapter="testimonials"] blockquote')] .map((item) => ({ direction: item.dataset.revealFrom, distance: getComputedStyle(item).getPropertyValue('--motion-translate-x').trim(), })) JS; $page->resize(1440, 1000); $page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'); $desktop = $page->script($readDistances); $page->resize(390, 844); $page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))'); $mobile = $page->script($readDistances); expect($desktop)->toBe([ ['direction' => 'left', 'distance' => 'calc(-1 * 24px)'], ['direction' => 'right', 'distance' => '24px'], ]); expect($mobile)->toBe([ ['direction' => 'left', 'distance' => 'calc(-1 * 16px)'], ['direction' => 'right', 'distance' => '16px'], ]); }); it('keeps content and navigation usable without javascript', function (): void { $this->visit('/', [ 'javaScriptEnabled' => false, ]) ->assertVisible('#hero-heading') ->assertVisible('[data-testid="home-primary-cta"]') ->click('[data-testid="home-primary-cta"]') ->assertPathIs('/contato'); }); it('keeps targets final when intersection observer is unavailable', function (): void { $page = $this->visit('/'); $rawPage = $page->page(); $url = $rawPage->url(); $rawPage->context()->addInitScript('window.IntersectionObserver = undefined;'); $rawPage->goto($url); $state = $page->script(<<<'JS' () => ({ enhancementEnabled: document.documentElement.hasAttribute('data-motion'), pagesFinal: [...document.querySelectorAll('[data-motion="page-open"]')] .every((node) => node.classList.contains('is-active')), revealsFinal: [...document.querySelectorAll('[data-reveal]')] .every((node) => node.classList.contains('is-revealed')), }) JS); expect($state)->toBe([ 'enhancementEnabled' => false, 'pagesFinal' => true, 'revealsFinal' => true, ]); }); it('allows cta activation while its opening is in progress', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'no-preference', ]); $page->script(<<<'JS' () => { const opening = document.querySelector('[data-motion="page-open"]'); opening?.classList.remove('is-active'); requestAnimationFrame(() => opening?.classList.add('is-active')); return true; } JS); $page->click('[data-testid="home-primary-cta"]') ->assertPathIs('/contato'); }); it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void { $case = PortfolioCase::factory()->published()->create([ 'slug' => 'casamento-jardim', ]); $routes = [ '/', '/servicos', '/portfolio', '/portfolio/'.$case->slug, '/sobre', '/contato', '/privacidade', '/__missing-public-motion', ]; foreach ([[1440, 1000], [390, 844]] as [$width, $height]) { foreach ($routes as $route) { $page = $this->visit($route, [ 'reducedMotion' => 'no-preference', ])->resize($width, $height); $overflow = $page->script('() => document.documentElement.scrollWidth - document.documentElement.clientWidth'); expect($overflow)->toBeLessThanOrEqual(0); } } }); it('updates the active chapter while scrolling the home dossier', function (): void { $page = $this->visit('/', [ 'reducedMotion' => 'no-preference', ]); $page->resize(1440, 1000); $before = $page->script(<<<'JS' () => document.querySelector('[data-chapter-index] [aria-current="true"]')?.getAttribute('href') ?? null JS); $page->script(<<<'JS' () => { const target = document.querySelector('#method-heading'); target?.scrollIntoView({ block: 'start' }); window.dispatchEvent(new Event('scroll')); return Boolean(target); } JS); $page->script(<<<'JS' () => new Promise((resolve) => { let frames = 0; const check = () => { const href = document.querySelector('[data-chapter-index] [aria-current="true"]')?.getAttribute('href'); if (href === '#method-heading' || frames > 60) { resolve(href); return; } frames += 1; requestAnimationFrame(check); }; check(); }) JS); $after = $page->script(<<<'JS' () => document.querySelector('[data-chapter-index] [aria-current="true"]')?.getAttribute('href') ?? null JS); expect($before)->not->toBeNull(); expect($after)->toBe('#method-heading'); }); it('applies CSS numbering to only rendered home folios in chapter order', function (): void { PortfolioCase::factory()->published()->create([ 'is_featured' => true, ]); Testimonial::factory()->published()->create([ 'quote' => 'Relato publicado.', ]); $page = $this->visit('/', [ 'reducedMotion' => 'reduce', ]); $numbering = $page->script(<<<'JS' () => { const flow = document.querySelector('.home-chapters'); const chapters = Array.from(document.querySelectorAll('.home-chapter')); const folios = Array.from(document.querySelectorAll('[data-home-folio]')); return { reset: flow ? getComputedStyle(flow).counterReset : '', increments: chapters.map((chapter) => getComputedStyle(chapter).counterIncrement), labels: folios.map((folio) => folio.querySelector('[data-home-folio-label]')?.textContent?.trim() ?? ''), contents: folios.map((folio) => { const number = folio.querySelector('[data-home-folio-number]'); return number ? getComputedStyle(number, '::before').content.replace(/["']/g, '') : ''; }), }; } JS); expect($numbering['reset'])->toContain('home-chapter -1'); expect($numbering['increments'])->each->toContain('home-chapter 1'); expect($numbering['labels'])->toBe([ 'Capa', 'Manifesto', 'Serviços', 'Portfólio', 'Método', 'Depoimentos', 'A Amare', 'Próximo passo', ]); expect($numbering['contents'])->each->toBe('counter(home-chapter, decimal-leading-zero)'); });