24-10: redesign integration verification — contrast fix, booking component root fix, tests; QA Review
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\WorkingHour;
|
||||
|
||||
beforeEach(function () {
|
||||
foreach (range(0, 6) as $dow) {
|
||||
WorkingHour::factory()->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, '<h1'))->toBe(1, "$locale $url h1");
|
||||
|
||||
preg_match_all('/<img\b[^>]*>/', $html, $imgs);
|
||||
foreach ($imgs[0] as $img) {
|
||||
expect($img)->toMatch('/\balt="[^"]+"/', "$locale $url img alt");
|
||||
}
|
||||
|
||||
$text = strip_tags(preg_replace('~<(script|style)\b.*?</\1>~s', '', $html));
|
||||
expect(preg_match('/(?<![\w.@\/-])(site|home|about|booking|posts|footer|pages|common|navigation|accessibility)\.[a-z][a-z0-9_]+(?![\w.-])/', $text, $m))
|
||||
->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('<?xml encoding="utf-8"?>'.$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');
|
||||
});
|
||||
Reference in New Issue
Block a user