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:
Naser Mansour
2026-10-31 19:03:57 +02:00
parent 01ff077a5c
commit 9263966266
6 changed files with 168 additions and 4 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: 24-10 id: 24-10
title: 24-10 Redesign Integration Verification title: 24-10 Redesign Integration Verification
status: Ready for Dev status: QA Review
date: 2026-10-31 date: 2026-10-31
parent: 24 parent: 24
--- ---
+41
View File
@@ -0,0 +1,41 @@
# Manual tests — 24-10 Redesign Integration Verification
## Done by the QA pass (2026-10-31)
Automated (`tests/Feature/Design/RedesignIntegrationTest.php`, 6 tests, plus the whole Design directory): for all eight public pages in Arabic
and English — exactly one h1, alt text on every image, no raw translation key text, no "Unsplash" or "mockup"; login and forgot-password carry the
auth layer; admin pages (dashboard, pending bookings, posts), client pages (dashboard, booking) and the Nablus form load none of the public assets;
the booking form on `/` and `/booking` sits inside its own Livewire component; WCAG contrast measured.
In a real browser (Chrome via Playwright, libra.test):
- Home hero balance level after load; with a real mouse over the frame it tips to about +8.4° and returns to level after leaving.
- Finale balance −12° at the top, level (≈0°) at the bottom; progress bar scaleX(1) at the bottom; nav gets `scrolled`; no duplicate ids; 44/44 reveals.
- Five `wire:navigate` hops (About, Articles, Booking, Terms, Home): hero balance still level, reveals work, finale still levels.
- Reduced motion emulated: 44/44 reveals visible immediately, all three balances `rotate(0 200 110)`.
- Focus ring on the first nav link: solid 2px gold.
- Booking from the home card: day strip loads days and slots, selecting a day/slot works, confirm step, Confirm lands on `/booking/success`; one pending guest consultation created.
- Booking from `/booking`: month calendar, slot, form, confirm, success page; one pending guest consultation created.
- Both test consultations (`qa-24-10-*@example.invalid`) and the 4 queue jobs they created were deleted; local DB back to 12 consultations / 12 jobs.
- Metrics sweep at 390/768/1440 in Arabic: no horizontal scroll on the pages listed in the ledger entry; h1 count 1 on public pages.
Contrast (measured): body ink on cream 14.3:1; muted on cream 5.7:1; gold-light on forest 5.6:1; on footer 6.5:1; ink on gold button 4.9:1; olive links 6.5:1;
gold-ink eyebrow/meta text **3.9:1 / 4.4:1 with the original `#8A7555` — below AA — darkened to `#7A6648`: 4.9:1 on cream, 5.5:1 on white**; footer legal line
raised from 75% to 90% opacity (4.3 → 5.6:1). Ruling logged.
## Defect found and fixed in this pass
The booking component's template began with a Blade `@if`, so Livewire did not attach its root: on `/` and `/booking` the form fields were not bound to the
component (console warnings "property does not exist on component pages.home"). The server-side tests passed anyway because they mount the component alone.
Fixed by giving the template a single root `<div>`; a test now checks the nearest Livewire root of the form.
## Still for you
- [ ] Look at each page once in Arabic and English at a phone width (both languages were checked by tests, but only Arabic by the width sweep).
- [ ] Tab through navigation, footer and the booking card with the keyboard: gold ring visible everywhere.
- [ ] In the admin (`/admin/bookings/pending`) confirm you can see a real test booking if you make one (the two I made are deleted).
- [ ] Open the admin and client dashboards and the Nablus workshop form: look unchanged.
## Open items for your decision (not fixed — outside the files this task may edit)
- `/login` and `/forgot-password` have no `<h1>` (Flux heading renders a div) and three inputs without a programmatic label: those come from `resources/views/livewire/auth/*`, which tasks 24-9/24-10 forbid editing.
- Old tests now stale against the redesign (they assert the previous markup/copy): in `HomePageTest`, `AboutPageTest`, `NavigationTest` (language toggle), `ResponsiveDesignTest` (public layout padding), plus failures that were already there before the redesign (footer copyright wording, `AuthBackgroundPatternTest`, focus-visible in `app.css`, admin `role="main"`).
+1 -1
View File
@@ -146,4 +146,4 @@
| 24-7 | 24-7 About Page | QA Review | | 24-7 | 24-7 About Page | QA Review |
| 24-8 | 24-8 Articles and Legal Pages | QA Review | | 24-8 | 24-8 Articles and Legal Pages | QA Review |
| 24-9 | 24-9 Auth Pages Type and Controls | QA Review | | 24-9 | 24-9 Auth Pages Type and Controls | QA Review |
| 24-10 | 24-10 Redesign Integration Verification | Ready for Dev | | 24-10 | 24-10 Redesign Integration Verification | QA Review |
+2 -2
View File
@@ -4,7 +4,7 @@
/* ---------- Tokens ---------- */ /* ---------- Tokens ---------- */
body.site{ body.site{
--forest:#2D3624; --forest-2:#232A1C; --gold:#A68966; --gold-ink:#8A7555; --gold-lt:#C4A882; --forest:#2D3624; --forest-2:#232A1C; --gold:#A68966; --gold-ink:#7A6648; --gold-lt:#C4A882;
--cream:#F4F1EA; --olive:#4A5D23; --ink:#1E2219; --muted:#5B6152; --line:rgba(45,54,36,.14); --cream:#F4F1EA; --olive:#4A5D23; --ink:#1E2219; --muted:#5B6152; --line:rgba(45,54,36,.14);
--display:"Markazi Text","Times New Roman",serif; --body:"IBM Plex Sans Arabic",system-ui,sans-serif; --display:"Markazi Text","Times New Roman",serif; --body:"IBM Plex Sans Arabic",system-ui,sans-serif;
--r:10px; --ease:cubic-bezier(.2,.7,.2,1); --r:10px; --ease:cubic-bezier(.2,.7,.2,1);
@@ -110,7 +110,7 @@ html[dir=ltr] body.site .foot a:hover{transform:translateX(3px)}
body.site .foot .brand b{color:var(--cream)} body.site .foot .brand b{color:var(--cream)}
body.site .foot .brand small{color:var(--gold-lt)} body.site .foot .brand small{color:var(--gold-lt)}
body.site .foot p{margin:16px 0 0;max-width:26em} body.site .foot p{margin:16px 0 0;max-width:26em}
body.site .site-footer .legal{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid rgba(244,241,234,.1);margin-top:48px;padding-top:22px;font-size:.8rem;color:rgba(196,168,130,.75)} body.site .site-footer .legal{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid rgba(244,241,234,.1);margin-top:48px;padding-top:22px;font-size:.8rem;color:rgba(196,168,130,.9)}
body.site .foot span{display:block;padding:10px 0} body.site .foot span{display:block;padding:10px 0}
body.site .foot a.brand{display:flex;padding:0} body.site .foot a.brand{display:flex;padding:0}
body.site .foot .brand span{display:block;padding:0} body.site .foot .brand span{display:block;padding:0}
@@ -196,6 +196,7 @@ new #[Layout('components.layouts.public')] class extends Component
} }
}; ?> }; ?>
<div>
@if($compact) @if($compact)
@if($isLoggedIn) @if($isLoggedIn)
<div class="book-r book-r-note"> <div class="book-r book-r-note">
@@ -380,3 +381,4 @@ new #[Layout('components.layouts.public')] class extends Component
</div> </div>
</section> </section>
@endif @endif
</div>
@@ -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');
});