Skip to content

Commit 1e7b947

Browse files
committed
Fix duplicate landmark on home page
1 parent b595c09 commit 1e7b947

4 files changed

Lines changed: 54 additions & 243 deletions

File tree

axe-results-violations.json

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,72 @@
11
[
22
{
3-
"id": "region",
3+
"id": "landmark-unique",
44
"impact": "moderate",
55
"tags": [
6-
"cat.keyboard",
7-
"best-practice",
8-
"RGAAv4",
9-
"RGAA-9.2.1"
6+
"cat.semantics",
7+
"best-practice"
108
],
11-
"description": "Ensure all page content is contained by landmarks",
12-
"help": "All page content should be contained by landmarks",
13-
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/region?application=playwright",
9+
"description": "Ensure landmarks are unique",
10+
"help": "Landmarks should have a unique role or role/label/title (i.e. accessible name) combination",
11+
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/landmark-unique?application=playwright",
1412
"nodes": [
1513
{
1614
"any": [
1715
{
18-
"id": "region",
16+
"id": "landmark-is-unique",
1917
"data": {
20-
"isIframe": false
18+
"role": "region",
19+
"accessibleText": "building the future of software development"
2120
},
22-
"relatedNodes": [],
21+
"relatedNodes": [
22+
{
23+
"html": "<section class=\"bg-content rounded-2xl p-8 md:p-12 overflow-hidden relative\" aria-labelledby=\"home-about-title\">",
24+
"target": [
25+
".md\\:p-12"
26+
]
27+
}
28+
],
2329
"impact": "moderate",
24-
"message": "Some page content is not contained by landmarks"
30+
"message": "The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable"
2531
}
2632
],
2733
"all": [],
2834
"none": [],
2935
"impact": "moderate",
30-
"html": "<div class=\"shrink-0\">",
36+
"html": "<section class=\"container mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8 lg:py-12\" aria-labelledby=\"home-about-title\">",
3137
"target": [
32-
"#breadcrumbs > div:nth-child(2)"
38+
".md\\:py-8.lg\\:py-12[aria-labelledby=\"home-about-title\"]"
3339
],
34-
"failureSummary": "Fix any of the following:\n Some page content is not contained by landmarks"
40+
"failureSummary": "Fix any of the following:\n The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable"
41+
},
42+
{
43+
"any": [
44+
{
45+
"id": "landmark-is-unique",
46+
"data": {
47+
"role": "region",
48+
"accessibleText": "technologies & expertise"
49+
},
50+
"relatedNodes": [
51+
{
52+
"html": "<skills-carousel class=\"block\" role=\"region\" aria-roledescription=\"carousel\" aria-labelledby=\"home-technologies-title\" data-carousel-autoplay=\"paused\" data-carousel-ready=\"true\">",
53+
"target": [
54+
"skills-carousel"
55+
]
56+
}
57+
],
58+
"impact": "moderate",
59+
"message": "The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable"
60+
}
61+
],
62+
"all": [],
63+
"none": [],
64+
"impact": "moderate",
65+
"html": "<section class=\"container mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8 lg:py-12 hidden md:block -mb-12\" aria-labelledby=\"home-technologies-title\">",
66+
"target": [
67+
".md\\:block"
68+
],
69+
"failureSummary": "Fix any of the following:\n The landmark must have a unique aria-label, aria-labelledby, or title to make landmarks distinguishable"
3570
}
3671
]
3772
}

src/pages/index.astro

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const sectionClasses = 'container mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8 lg:p
3030
<!-- Intro Section -->
3131
<section
3232
class:list={[sectionClasses]}
33-
aria-labelledby="home-about-title"
3433
>
3534
<Intro
3635
pretitle="What I Do"
@@ -103,7 +102,6 @@ const sectionClasses = 'container mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8 lg:p
103102
<!-- Skills/Technologies Preview -->
104103
<section
105104
class:list={[sectionClasses, 'hidden md:block -mb-12']}
106-
aria-labelledby="home-technologies-title"
107105
>
108106
<Skills title="Technologies & Expertise" />
109107
</section>

test/e2e/specs/11-accessibility/axe.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ describe('WCAG Compliance', () => {
3232
await runAcrossPages(page, 'check forms', async (url) => {
3333
await page.goto(url)
3434
const results = await new AxeBuilder({ page: page.page })
35-
.withTags(['cat.keyboard'])
36-
.disableRules('color-contrast-enhanced')
35+
.withTags(['cat.semantics'])
36+
//.disableRules('color-contrast-enhanced')
3737
.analyze()
3838

3939
const incompleteResultsString = JSON.stringify(results.incomplete, null, 2)

test/e2e/specs/11-accessibility/high-contrast-wcag-compliance.spec.ts

Lines changed: 2 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,12 @@
22
* WCAG Compliance Tests
33
* Tests for Web Content Accessibility Guidelines compliance
44
*/
5-
import { BasePage, test, expect } from '@test/e2e/helpers'
5+
import { BasePage, describe, test, expect } from '@test/e2e/helpers'
66
import { waitForAnimationFrames } from '@test/e2e/helpers/waitHelpers'
77

88
// @TODO: The purpose of this file is to have a special test for the "a11y" theme that should meet the higher WCAG 2.2 standards. Those standards require higher contrast ratios and other improvements.
99

10-
test.describe('WCAG Compliance', () => {
11-
/**
12-
* target-size rule is disabled in Axe by default!!
13-
*
14-
* Axe can checks if touch targets are at least 24x24 CSS pixels. If a target is smaller
15-
* than 24x24 pixels, it must be at least 24 pixels away from any other touch target.
16-
* A touch target size of at least 44x44 pixels is part of the WCAG 2.1 AAA guidelines,
17-
* which is a more stringent level of compliance,
18-
*/
19-
test.skip('@wip touch targets are at least 44x44 pixels', async ({ page: playwrightPage }) => {
20-
const page = await BasePage.init(playwrightPage)
21-
await page.goto('/')
22-
23-
const buttons = page.page.locator('button, a')
24-
const count = await buttons.count()
25-
26-
let validButtonsChecked = 0
27-
for (let i = 0; i < count && validButtonsChecked < 10; i++) {
28-
const button = buttons.nth(i)
29-
const box = await button.boundingBox()
30-
31-
if (box && (await button.isVisible()) && box.width > 5 && box.height > 5) {
32-
// 44x44 is WCAG AAA, 24x24 is AA
33-
expect(box.width).toBeGreaterThan(20)
34-
expect(box.height).toBeGreaterThan(20)
35-
validButtonsChecked++
36-
}
37-
}
38-
39-
// Ensure we actually checked some buttons
40-
expect(validButtonsChecked).toBeGreaterThan(0)
41-
})
42-
10+
describe('WCAG Compliance', () => {
4311
/**
4412
* Axe by defaults checks for color contrast of at least 4.5:1 for small text or 3:1 for
4513
* large text, even if text is part of an image. Large text has been defined in the
@@ -67,194 +35,4 @@ test.describe('WCAG Compliance', () => {
6735
expect(contrast.color).toBeTruthy()
6836
}
6937
})
70-
71-
/**
72-
* Axe checks for visible focus indicators. The Keyboard Guided Test in axe DevTools Pro
73-
* provides more comprehensive automated testing by simulating a keyboard navigation through
74-
* all focusable elements on the page. It can identify issues such as missing focus indicators,
75-
* missing ARIA roles, and other keyboard-related accessibility problems. Manual testing is
76-
* still necessary to ensure all specific requirements of the accessibility standards, such as
77-
* those in WCAG 2.2, are fully met. This includes checking that the focus indicator meets
78-
* minimum size and contrast criteria relative to adjacent colors, not just the background color.
79-
*/
80-
test.skip('@ready focus indicators are visible', async ({ page: playwrightPage }) => {
81-
const page = await BasePage.init(playwrightPage)
82-
await page.goto('/')
83-
84-
await page.pressKey('Tab')
85-
await page.pressKey('Tab')
86-
87-
interface FocusIndicator {
88-
outline: string,
89-
outlineColor: string,
90-
outlineWidth: string,
91-
}
92-
93-
const focusIndicator = await page.evaluate<FocusIndicator | null>(() => {
94-
const el = document.activeElement
95-
if (!el) return null
96-
97-
const styles = window.getComputedStyle(el)
98-
return {
99-
outline: styles.outline,
100-
outlineColor: styles.outlineColor,
101-
outlineWidth: styles.outlineWidth,
102-
}
103-
})
104-
105-
// Should have visible focus indicator
106-
expect(focusIndicator?.outline !== 'none' || focusIndicator?.outlineWidth !== '0px').toBe(true)
107-
})
108-
109-
/**
110-
* Axe checks that the user-scalable="no" parameter is not present in the <meta name="viewport">
111-
* element and the maximum-scale parameter is not less than 2.
112-
*/
113-
test.skip('@ready page can be zoomed to 200%', async ({ page: playwrightPage }) => {
114-
const page = await BasePage.init(playwrightPage)
115-
116-
await page.goto('/')
117-
118-
// Zoom in
119-
await page.evaluate(() => {
120-
document.body.style.zoom = '2'
121-
})
122-
123-
await waitForAnimationFrames(page.page, 30)
124-
125-
// Content should still be accessible
126-
await page.expectMainElement()
127-
128-
// No horizontal scroll should be needed at 200% zoom (in most cases)
129-
const hasHorizontalScroll = await page.evaluate(() => {
130-
return document.documentElement.scrollWidth > window.innerWidth
131-
})
132-
133-
// This may be acceptable in some cases, just checking
134-
expect(typeof hasHorizontalScroll).toBe('boolean')
135-
})
136-
137-
/**
138-
* Axe checks that links are distinguishable from text by verifying that they have
139-
* a visual distinction that does not rely solely on color, such as an underline. It
140-
* checks for a color contrast of at least \(3:1\) between the link and the surrounding
141-
* text, and if the contrast is less, it requires a non-color visual distinction like
142-
* an underline. Axe also checks if links have a distinct style on focus and hover.
143-
*/
144-
test.skip('@ready links are distinguishable from text', async ({ page: playwrightPage }) => {
145-
const page = await BasePage.init(playwrightPage)
146-
147-
await page.goto('/')
148-
149-
const link = page.page.locator('a[href]').first()
150-
const styles = await link.evaluate((el) => {
151-
const computed = window.getComputedStyle(el)
152-
return {
153-
textDecoration: computed.textDecoration,
154-
fontWeight: computed.fontWeight,
155-
}
156-
})
157-
158-
// Should have underline or other non-color indicator
159-
const hasUnderline = styles.textDecoration.includes('underline')
160-
const isBold = parseInt(styles.fontWeight) >= 600
161-
162-
// Test passes if there's some non-color distinction
163-
expect(hasUnderline || isBold || typeof styles.textDecoration === 'string').toBe(true)
164-
})
165-
166-
/**
167-
* Axe checks that no content flashes more than 3 times per second. It identifies violations
168-
* of both the general flash threshold and the more restrictive "three flashes" rule.
169-
*/
170-
test.skip('@ready no content flashes more than 3 times per second', async ({ page: playwrightPage }) => {
171-
const page = await BasePage.init(playwrightPage)
172-
await page.goto('/')
173-
174-
// Check for animations
175-
const animations = await page.evaluate(() => {
176-
const elements = document.querySelectorAll('*')
177-
const animated = []
178-
179-
elements.forEach((el) => {
180-
const styles = window.getComputedStyle(el)
181-
if (styles.animation !== 'none' || styles.transition !== 'all 0s ease 0s') {
182-
animated.push({
183-
animation: styles.animation,
184-
transition: styles.transition,
185-
})
186-
}
187-
})
188-
189-
return animated.length
190-
})
191-
192-
// Test passes - just checking for animations presence
193-
expect(animations).toBeGreaterThanOrEqual(0)
194-
})
195-
196-
/**
197-
* Axe does not check if prefers-reduced-motion is respected; this is considered
198-
* a complex, context-dependent check that requires manual inspection.
199-
*/
200-
test.skip('@ready page is usable without motion', async ({ page: playwrightPage }) => {
201-
const page = await BasePage.init(playwrightPage)
202-
203-
await page.page.emulateMedia({ reducedMotion: 'reduce' })
204-
await page.goto('/')
205-
206-
// Check that animations are disabled/reduced
207-
const hasReducedMotion = await page.evaluate(() => {
208-
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
209-
})
210-
211-
expect(hasReducedMotion).toBe(true)
212-
213-
// Content should still be accessible
214-
await page.expectMainElement()
215-
})
216-
217-
/**
218-
* Axe detects if form error indicators are technically accessible to assistive
219-
* technologies like screen readers. A manual check is necessary to confirm that
220-
* correctly entered information remains in the form after a validation error.
221-
* When a page reloads after a server-side error, a manual test is needed to
222-
* ensure that focus is moved to the top of the form or the first field with an
223-
* error. For client-side validation, a manual check confirms that focus shifts
224-
* to the invalid field or an error summary.
225-
*/
226-
test.skip('@wip form errors are clearly identified', async ({ page: playwrightPage }) => {
227-
const page = await BasePage.init(playwrightPage)
228-
229-
await page.goto('/contact')
230-
231-
const submitButton = page.page.locator('button[type="submit"]').first()
232-
await submitButton.click()
233-
234-
const errors = page.page.locator('[data-error], .error, [role="alert"]')
235-
await errors.first().waitFor({ state: 'visible' })
236-
const count = await errors.count()
237-
238-
expect(count).toBeGreaterThan(0)
239-
240-
// Error should be descriptive
241-
const errorText = await errors.first().textContent()
242-
expect(errorText?.trim().length).toBeGreaterThan(5)
243-
})
244-
245-
/**
246-
* ?
247-
*/
248-
test.skip('@ready time limits can be extended', async ({ page: playwrightPage }) => {
249-
const page = await BasePage.init(playwrightPage)
250-
251-
await page.goto('/')
252-
253-
// Check for timers or session warnings
254-
const timer = page.page.locator('[data-timer], [data-timeout]')
255-
const count = await timer.count()
256-
257-
// Test passes regardless - just checking for presence
258-
expect(count).toBeGreaterThanOrEqual(0)
259-
})
26038
})

0 commit comments

Comments
 (0)