fix(marketing): A-2.7b WCAG 2.2 AA polish + JSON-LD test hardening
- Drop role="region" from FAQ panels (had no accessible name — axe-core violation; disclosure pattern with button + aria-controls + aria-expanded is sufficient per WAI-APG accordion guidance) - Add focus-visible:outline-2 outline-brand-b1 outline-offset-2 to FAQ buttons (WCAG 2.2 AA 2.4.7 Focus Not Obscured + 2.4.11 Focus Appearance — Safari default focus indicator is unreliable) - Sweep pre-existing text-white/50 on Hero social proof microcopy → /70 (branch-wide WCAG floor; recurring landmine flagged at A-2.7a review) - Strengthen test_faq_jsonld_schema_present to json.loads() the extracted block and validate the FAQPage schema shape (regression guard for future content edits with unescaped backslashes/quotes)
This commit is contained in:
@@ -3701,6 +3701,22 @@
|
|||||||
outline-style: none;
|
outline-style: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.focus-visible\:outline-2 {
|
||||||
|
&:focus-visible {
|
||||||
|
outline-style: var(--tw-outline-style);
|
||||||
|
outline-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus-visible\:outline-offset-2 {
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus-visible\:outline-brand-b1 {
|
||||||
|
&:focus-visible {
|
||||||
|
outline-color: #0062ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
.active\:scale-95 {
|
.active\:scale-95 {
|
||||||
&:active {
|
&:active {
|
||||||
--tw-scale-x: 95%;
|
--tw-scale-x: 95%;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Social proof microcopy — defensible: refers to pre-launch waitlist + factual ordres pros count #}
|
{# Social proof microcopy — defensible: refers to pre-launch waitlist + factual ordres pros count #}
|
||||||
<p class="mt-8 text-sm text-white/50 animate-tc-fade-in-up" style="animation-delay: 400ms; animation-fill-mode: backwards;">
|
<p class="mt-8 text-sm text-white/70 animate-tc-fade-in-up" style="animation-delay: 400ms; animation-fill-mode: backwards;">
|
||||||
<span class="inline-flex items-center gap-1.5">
|
<span class="inline-flex items-center gap-1.5">
|
||||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" class="text-brand-b3" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" class="text-brand-b3" aria-hidden="true">
|
||||||
<path d="M10 2L3 5v5.5c0 4.04 2.84 7.85 7 8.5 4.16-.65 7-4.46 7-8.5V5l-7-3z"/>
|
<path d="M10 2L3 5v5.5c0 4.04 2.84 7.85 7 8.5 4.16-.65 7-4.46 7-8.5V5l-7-3z"/>
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
<div x-data="{ open: false }" class="py-2" role="listitem">
|
<div x-data="{ open: false }" class="py-2" role="listitem">
|
||||||
<h3>
|
<h3>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="w-full flex items-center justify-between gap-4 py-4 text-left hover:bg-brand-bg/50 transition-colors rounded-md px-2"
|
class="w-full flex items-center justify-between gap-4 py-4 text-left hover:bg-brand-bg/50 transition-colors rounded-md px-2 focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2"
|
||||||
@click="open = !open"
|
@click="open = !open"
|
||||||
:aria-expanded="open.toString()"
|
:aria-expanded="open.toString()"
|
||||||
aria-controls="faq-panel-{{ loop.index }}">
|
aria-controls="faq-panel-{{ loop.index }}">
|
||||||
@@ -495,8 +495,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div id="faq-panel-{{ loop.index }}"
|
<div id="faq-panel-{{ loop.index }}"
|
||||||
x-show="open"
|
x-show="open"
|
||||||
x-transition.opacity.duration.200ms
|
x-transition.opacity.duration.200ms>
|
||||||
role="region">
|
|
||||||
<p class="px-2 pb-4 text-sm text-brand-navy/80 leading-relaxed">{{ item.a | safe }}</p>
|
<p class="px-2 pb-4 text-sm text-brand-navy/80 leading-relaxed">{{ item.a | safe }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -696,6 +696,23 @@ def test_faq_jsonld_schema_present():
|
|||||||
assert ' ' not in body[body.find('"FAQPage"'):body.find('</script>', body.find('"FAQPage"'))], \
|
assert ' ' not in body[body.find('"FAQPage"'):body.find('</script>', body.find('"FAQPage"'))], \
|
||||||
"JSON-LD must not contain raw ' ' entities — strip them server-side"
|
"JSON-LD must not contain raw ' ' entities — strip them server-side"
|
||||||
|
|
||||||
|
# M-1 hardening: actually parse the JSON-LD to catch malformed JSON regressions
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
match = re.search(r'<script type="application/ld\+json">(.*?)</script>',
|
||||||
|
body, re.DOTALL)
|
||||||
|
assert match, "JSON-LD block not found"
|
||||||
|
parsed = json.loads(match.group(1))
|
||||||
|
assert parsed['@context'] == 'https://schema.org'
|
||||||
|
assert parsed['@type'] == 'FAQPage'
|
||||||
|
assert isinstance(parsed['mainEntity'], list)
|
||||||
|
assert len(parsed['mainEntity']) == 7, "FAQPage must contain exactly 7 questions"
|
||||||
|
for q in parsed['mainEntity']:
|
||||||
|
assert q['@type'] == 'Question'
|
||||||
|
assert q['acceptedAnswer']['@type'] == 'Answer'
|
||||||
|
assert q['name'].strip(), "Question name must not be empty"
|
||||||
|
assert q['acceptedAnswer']['text'].strip(), "Answer text must not be empty"
|
||||||
|
|
||||||
|
|
||||||
def test_cta_final_section():
|
def test_cta_final_section():
|
||||||
"""CTA final section with mailto pré-inscription + ghost button to #tarifs."""
|
"""CTA final section with mailto pré-inscription + ghost button to #tarifs."""
|
||||||
|
|||||||
Reference in New Issue
Block a user