create(['day_of_week' => $dow, 'is_active' => true, 'start_time' => '09:00', 'end_time' => '13:00']); } Post::factory()->published()->count(3)->create(); Page::factory()->terms()->create(); Page::factory()->create(['slug' => 'privacy', 'title_en' => 'Privacy Policy', 'title_ar' => 'سياسة الخصوصية']); }); function publicUrls(): array { return ['/', '/about', '/posts', '/posts/'.Post::first()->id, '/booking', '/booking/success', '/page/terms', '/page/privacy']; } test('public pages: one h1, alt text, no raw keys, no mockup text, in both languages', function () { foreach (['ar', 'en'] as $locale) { foreach (publicUrls() as $url) { $html = $this->withSession(['locale' => $locale])->get($url)->assertOk()->getContent(); expect(substr_count($html, 'toBe(1, "$locale $url h1"); preg_match_all('/]*>/', $html, $imgs); foreach ($imgs[0] as $img) { expect($img)->toMatch('/\balt="[^"]+"/', "$locale $url img alt"); } $text = strip_tags(preg_replace('~<(script|style)\b.*?~s', '', $html)); expect(preg_match('/(?toBe(0, "$locale $url raw key: ".($m[0] ?? '')); expect($html)->not->toContain('Unsplash')->not->toContain('mockup'); expect($html)->toContain('site-progress')->toContain('id="site-nav"'); } } }); test('auth pages carry the auth layer in both languages', function () { foreach (['ar', 'en'] as $locale) { foreach (['/login', '/forgot-password'] as $url) { $html = $this->withSession(['locale' => $locale])->get($url)->assertOk()->getContent(); expect($html)->toContain('site-auth')->not->toContain('Unsplash')->not->toContain('mockup'); } } }); test('unchanged areas load none of the public assets', function () { $admin = User::factory()->admin()->create(); foreach (['/admin/dashboard', '/admin/bookings/pending', '/admin/posts'] as $url) { $html = $this->actingAs($admin)->get($url)->assertOk()->getContent(); expect($html)->not->toContain('public.css')->not->toContain('public.js')->not->toContain('class="site')->not->toContain('site-auth'); } auth()->logout(); $client = User::factory()->individual()->create(); foreach (['/client/dashboard', '/client/consultations/book'] as $url) { $html = $this->actingAs($client)->get($url)->assertOk()->getContent(); expect($html)->not->toContain('public.css')->not->toContain('public.js')->not->toContain('class="site')->not->toContain('site-auth'); } $nablus = $this->get('/forms/nablus-workshop')->assertOk()->getContent(); expect($nablus)->not->toContain('public.css')->not->toContain('class="site'); }); test('the public interactive controls carry no tabindex trap', function () { $html = $this->get('/')->assertOk()->getContent(); expect($html)->not->toMatch('/tabindex="[1-9]/'); expect($html)->toContain('aria-label="'.__('site.nav_menu').'"'); }); test('measured contrast of the redesign colour pairs meets WCAG AA', function () { $lum = function (string $hex) { [$r, $g, $b] = array_map(fn ($i) => hexdec(substr(ltrim($hex, '#'), $i, 2)) / 255, [0, 2, 4]); $f = fn ($c) => $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4; return 0.2126 * $f($r) + 0.7152 * $f($g) + 0.0722 * $f($b); }; $ratio = function (string $a, string $b) use ($lum) { [$hi, $lo] = $lum($a) >= $lum($b) ? [$lum($a), $lum($b)] : [$lum($b), $lum($a)]; return ($hi + 0.05) / ($lo + 0.05); }; expect($ratio('#1E2219', '#F4F1EA'))->toBeGreaterThanOrEqual(4.5); // body text on cream expect($ratio('#C4A882', '#2D3624'))->toBeGreaterThanOrEqual(4.5); // gold-lt small text on forest expect($ratio('#C4A882', '#232A1C'))->toBeGreaterThanOrEqual(4.5); // gold-lt on footer expect($ratio('#1E2219', '#A68966'))->toBeGreaterThanOrEqual(4.5); // text on the gold button expect($ratio('#7A6648', '#F4F1EA'))->toBeGreaterThanOrEqual(4.5); // gold-ink eyebrow on cream expect($ratio('#7A6648', '#FFFFFF'))->toBeGreaterThanOrEqual(4.5); // gold-ink post meta on white expect($ratio('#5B6152', '#F4F1EA'))->toBeGreaterThanOrEqual(4.5); // muted text on cream }); function nearestLivewireRoot(string $html, string $xpathTarget): ?string { $dom = new DOMDocument; @$dom->loadHTML(''.$html); $xp = new DOMXPath($dom); $node = $xp->query($xpathTarget)->item(0); while ($node instanceof DOMElement) { if ($node->hasAttribute('wire:id')) { return $node->getAttribute('wire:snapshot'); } $node = $node->parentNode; } return null; } test('the booking form on the home page and /booking sits inside its own Livewire component', function () { $home = $this->get('/')->assertOk()->getContent(); expect(nearestLivewireRoot($home, '//form[contains(@class,"book-r")]'))->toContain('guestName'); $page = $this->get('/booking')->assertOk()->getContent(); expect(nearestLivewireRoot($page, '//*[contains(@class,"book-r")]'))->toContain('guestName'); });