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:
+1
-1
@@ -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
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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"`).
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user