diff --git a/docs/tasks/24-10.md b/docs/tasks/24-10.md index ccac1ab..ab0d93e 100644 --- a/docs/tasks/24-10.md +++ b/docs/tasks/24-10.md @@ -1,7 +1,7 @@ --- id: 24-10 title: 24-10 Redesign Integration Verification -status: Ready for Dev +status: QA Review date: 2026-10-31 parent: 24 --- diff --git a/docs/tasks/manual-tests/24-10.md b/docs/tasks/manual-tests/24-10.md new file mode 100644 index 0000000..65a4d4d --- /dev/null +++ b/docs/tasks/manual-tests/24-10.md @@ -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 `
`; 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 `

` (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"`). diff --git a/docs/tasks/statuses.md b/docs/tasks/statuses.md index 9f58dd1..cce2ce7 100644 --- a/docs/tasks/statuses.md +++ b/docs/tasks/statuses.md @@ -146,4 +146,4 @@ | 24-7 | 24-7 About Page | QA Review | | 24-8 | 24-8 Articles and Legal Pages | 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 | diff --git a/resources/css/public.css b/resources/css/public.css index d3b4659..370280f 100644 --- a/resources/css/public.css +++ b/resources/css/public.css @@ -4,7 +4,7 @@ /* ---------- Tokens ---------- */ 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); --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); @@ -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 small{color:var(--gold-lt)} 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 a.brand{display:flex;padding:0} body.site .foot .brand span{display:block;padding:0} diff --git a/resources/views/livewire/pages/booking.blade.php b/resources/views/livewire/pages/booking.blade.php index fe6a7c5..b3407c5 100644 --- a/resources/views/livewire/pages/booking.blade.php +++ b/resources/views/livewire/pages/booking.blade.php @@ -196,6 +196,7 @@ new #[Layout('components.layouts.public')] class extends Component } }; ?> +
@if($compact) @if($isLoggedIn)
@@ -380,3 +381,4 @@ new #[Layout('components.layouts.public')] class extends Component
@endif +
diff --git a/tests/Feature/Design/RedesignIntegrationTest.php b/tests/Feature/Design/RedesignIntegrationTest.php new file mode 100644 index 0000000..a367061 --- /dev/null +++ b/tests/Feature/Design/RedesignIntegrationTest.php @@ -0,0 +1,121 @@ +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'); +});