Skip to content

Commit 29b3e3e

Browse files
authored
Merge pull request #698 from webstackdev/maintenance/update-e2e-cases
Maintenance/update e2e cases
2 parents aecf4af + 89e09bf commit 29b3e3e

19 files changed

Lines changed: 182 additions & 48 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,5 @@ npm run lint # full lint suite
119119
## Tech Stack
120120

121121
Astro, TypeScript, Lit, Tailwind CSS, Turso (libSQL), Sentry, Vercel, Playwright, Vitest, MJML, Workbox, GitHub Actions
122+
123+
test/e2e/specs/11-accessibility/high-contrast-wcag-compliance.spec.ts

src/components/CallToAction/Newsletter/index.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const descriptionId = `${idBase}-description`
3838

3939
<newsletter-form
4040
class="cta block print:hidden!"
41+
role="group"
4142
aria-labelledby={titleId}
4243
aria-describedby={descriptionId}
4344
>

src/components/Pages/Newsletter/Confirm/client/__tests__/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ describe('NewsletterConfirmElement web component', () => {
9191
expect(elements.expiredState.classList.contains('hidden')).toBe(false)
9292
expect(elements.loadingState.classList.contains('hidden')).toBe(true)
9393
expect(elements.statusAnnouncer.textContent).toBe('Confirmation link expired.')
94+
expect(document.activeElement).toBe(elements.expiredHeading)
9495
},
9596
{ data: { success: false, status: 'expired' } }
9697
)

src/components/Pages/Newsletter/Confirm/client/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,27 @@ export class NewsletterConfirmElement extends LitElement {
126126
}
127127

128128
private focusHeading(heading: HTMLElement): void {
129-
heading.focus()
129+
const attemptFocus = () => {
130+
if (!heading.isConnected) {
131+
return
132+
}
133+
134+
heading.focus()
135+
}
136+
137+
attemptFocus()
138+
139+
if (heading.ownerDocument.activeElement === heading) {
140+
return
141+
}
142+
143+
window.setTimeout(() => {
144+
if (heading.ownerDocument.activeElement === heading) {
145+
return
146+
}
147+
148+
attemptFocus()
149+
}, 0)
130150
}
131151

132152
private hideAllStates(): void {

test/e2e/helpers/cookieHelper.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,17 @@ export async function selectTheme(page: Page, themeId: string): Promise<void> {
166166
// Click the theme button
167167
// Use button selector to avoid matching <html data-theme="...">
168168
const themeButton = page.locator(`button[data-theme="${themeId}"]`)
169-
await themeButton.click()
169+
const viewport = page.viewportSize()
170+
const isMobile = Boolean(viewport && viewport.width < 768)
171+
172+
if (isMobile) {
173+
await themeButton.focus()
174+
await themeButton.evaluate((button: HTMLButtonElement) => {
175+
button.click()
176+
})
177+
} else {
178+
await themeButton.click()
179+
}
170180

171181
// Wait for current theme to update everywhere
172182
const html = page.locator('html')

test/e2e/helpers/fetchOverride.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,40 @@ const delay = async (delayMs: number): Promise<void> => {
4545
await new Promise<void>(resolve => setTimeout(resolve, delayMs))
4646
}
4747

48+
const isRouteAlreadyHandledError = (error: unknown): boolean => {
49+
return error instanceof Error && error.message.includes('Route is already handled')
50+
}
51+
52+
const safeContinueRoute = async (
53+
route: Route,
54+
overrides?: Parameters<Route['continue']>[0],
55+
): Promise<void> => {
56+
try {
57+
await route.continue(overrides)
58+
} catch (error) {
59+
if (isRouteAlreadyHandledError(error)) {
60+
return
61+
}
62+
63+
throw error
64+
}
65+
}
66+
67+
const safeFulfillRoute = async (
68+
route: Route,
69+
overrides: Parameters<Route['fulfill']>[0],
70+
): Promise<void> => {
71+
try {
72+
await route.fulfill(overrides)
73+
} catch (error) {
74+
if (isRouteAlreadyHandledError(error)) {
75+
return
76+
}
77+
78+
throw error
79+
}
80+
}
81+
4882
const withTimeout = async (promise: Promise<void>, timeoutMs: number, timeoutMessage: string): Promise<void> => {
4983
let timeoutId: ReturnType<typeof setTimeout> | undefined
5084
const timeoutPromise = new Promise<void>((_, reject) => {
@@ -130,13 +164,13 @@ const createFetchOverride = async (page: Page, options: OverrideOptions): Promis
130164
}
131165

132166
if (options.mode === 'spy') {
133-
await route.continue()
167+
await safeContinueRoute(route)
134168
return
135169
}
136170

137171
if (options.mode === 'delay') {
138172
await delay(options.delayMs)
139-
await route.continue()
173+
await safeContinueRoute(route)
140174
return
141175
}
142176

@@ -145,7 +179,7 @@ const createFetchOverride = async (page: Page, options: OverrideOptions): Promis
145179
...request.headers(),
146180
...options.headers,
147181
}
148-
await route.continue({ headers: mergedHeaders })
182+
await safeContinueRoute(route, { headers: mergedHeaders })
149183
return
150184
}
151185

@@ -159,15 +193,15 @@ const createFetchOverride = async (page: Page, options: OverrideOptions): Promis
159193

160194
const body = typeof resolvedBody === 'string' ? resolvedBody : JSON.stringify(resolvedBody)
161195

162-
await route.fulfill({
196+
await safeFulfillRoute(route, {
163197
status: options.status ?? 200,
164198
headers,
165199
body,
166200
})
167201
return
168202
}
169203

170-
await route.continue()
204+
await safeContinueRoute(route)
171205
}
172206

173207
await page.route(urlPattern, handler)

test/e2e/helpers/pageObjectModels/BreadCrumbPage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ export class BreadCrumbPage extends BasePage {
8383
async openFirstArticleDetail(options?: { navigationMode?: 'client' | 'fresh' }): Promise<void> {
8484
await this.navigateToListingDetail({
8585
listingPath: '/articles',
86-
linkSelector: 'main a[href^="/articles/"]',
86+
linkSelector: 'main a[href^="/deep-dive/"], main a[href^="/articles/"]',
8787
minSegments: 2,
88-
notFoundMessage: 'Could not find article detail link on /articles',
88+
notFoundMessage: 'Could not find deep-dive or article detail link on /articles',
8989
...options,
9090
})
9191
}

test/e2e/specs/01-smoke/dynamic-pages.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { BasePage, test, expect, setupConsoleErrorChecker, logConsoleErrors } from '@test/e2e/helpers'
77
import { wait } from '@test/e2e/helpers/waitTimeouts'
88

9-
const articleLinkSelector = 'a[href*="/articles/"]'
9+
const articleLinkSelector = 'a[href*="/deep-dive/"]'
1010
const serviceLinkSelector = 'a[href^="/services/"]:not([href="/services/"])'
1111
const caseStudyLinkSelector = 'a[href*="/case-studies/"]'
1212

@@ -54,8 +54,8 @@ test.describe('Dynamic Pages @smoke', () => {
5454
await expect(page.locator('main#main')).toBeVisible()
5555
await expect(page.locator('h1[id="article-title"]')).toBeVisible()
5656

57-
// Verify we're on an article page (URL should match pattern)
58-
expect(page.getCurrentUrl()).toMatch(/\/articles\/.+/)
57+
// Verify we're on a deep-dive detail page from the articles listing
58+
expect(page.getCurrentUrl()).toMatch(/\/deep-dive\/.+/)
5959
})
6060

6161
test('@ready service detail page loads', async ({ page: playwrightPage }) => {

test/e2e/specs/02-pages/articles.spec.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,25 @@ test.describe('Articles Page', () => {
1313
await page.goto('/articles')
1414

1515
await page.evaluate(selector => {
16-
const link = document.querySelector<HTMLAnchorElement>(selector)
17-
// eslint-disable-next-line custom-rules/enforce-centralized-events -- test-only handler in Playwright browser context
18-
link?.addEventListener(
16+
// Prevent Astro/client-side navigation before it can consume the click.
17+
document.addEventListener(
1918
'click',
2019
event => {
20+
const target = event.target
21+
if (!(target instanceof Element)) {
22+
return
23+
}
24+
25+
const link = target.closest<HTMLAnchorElement>(selector)
26+
if (!link) {
27+
return
28+
}
29+
2130
event.preventDefault()
31+
event.stopPropagation()
32+
event.stopImmediatePropagation()
2233
},
23-
{ once: true }
34+
{ capture: true, once: true }
2435
)
2536
}, linkSelector)
2637

@@ -31,12 +42,12 @@ test.describe('Articles Page', () => {
3142
const link = card.querySelector('a')
3243

3344
return {
34-
isFocused: link === document.activeElement,
45+
hasFocusVisible: link?.matches(':focus-visible') ?? false,
3546
opacity: afterStyles.opacity,
3647
}
3748
})
3849

39-
expect(overlayState.isFocused).toBe(true)
50+
expect(overlayState.hasFocusVisible).toBe(false)
4051
expect(overlayState.opacity).toBe('0')
4152
})
4253

@@ -64,10 +75,10 @@ test.describe('Articles Page', () => {
6475
const page = await BasePage.init(playwrightPage)
6576
await page.goto('/articles')
6677

67-
// Get the first article link
78+
// Get the first deep-dive link from the articles index page
6879
await page.click('article a')
69-
// Should navigate to an article detail page
70-
await page.expectUrl(/\/articles\/[^/]+/)
80+
// Should navigate to a deep-dive detail page
81+
await page.expectUrl(/\/deep-dive\/[^/]+/)
7182
})
7283

7384
test('@ready page subtitle displays', async ({ page: playwrightPage }) => {

test/e2e/specs/02-pages/tags.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ test.describe('Tags Index Page', () => {
2121
test('@ready tag counts display', async ({ page: playwrightPage }) => {
2222
const page = await BasePage.init(playwrightPage)
2323
await page.goto('/tags')
24-
// Each tag carousel should render at least one article card link
25-
await page.expectElementVisible('a[href^="/articles/"]')
24+
// Each tag carousel should render at least one deep-dive card link
25+
await page.expectElementVisible('a[href^="/deep-dive/"]')
2626
})
2727

2828
test('@ready responsive: mobile view renders correctly', async ({ page: playwrightPage }) => {

0 commit comments

Comments
 (0)