Skip to content

Commit 1bc837d

Browse files
committed
Fix broken test cases after refactor to lit and changing hero terminal text animation
1 parent 79ddb0c commit 1bc837d

7 files changed

Lines changed: 141 additions & 115 deletions

File tree

_TODO.md

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,6 @@ A bunch of our web components extend directly from HTMLElement instead of follow
280280
- Consent/Checkbox
281281
- Consent/Preferences
282282

283-
And the embla components: Carousel and Testimonials
284-
285283
The WebComponentModule type in this file is used throughout component scripts:
286284

287285
`src/components/scripts/@types/webComponentModule.ts`
@@ -292,84 +290,3 @@ There are testing fixtures that might be targeting HTMLElement instead of LitEle
292290
line 113 in `test/eslint/__tests__/enforce-centralized-events-rule.spec.ts`
293291

294292
interface `ElementWithTestProperties` in `test/e2e/assertions/index.ts`
295-
296-
/home/kevin/Repos/WebstackBuilders/CorporateWebsite/astro.webstackbuilders.com/src/components/Carousel/client/index.ts
297-
70:5 error Use `keyup with addButtonEventListeners or addWrapperEventListeners` from `elementListeners` for keyboard events (use keyup instead of keydown for better accessibility) instead of direct addEventListener. This ensures consistent accessibility support (isComposing check, repeat prevention, Enter/Escape key handling) custom-rules/enforce-centralized-events
298-
299-
✖ 1 problem (1 error, 0 warnings)
300-
301-
302-
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
303-
304-
FAIL src/components/Testimonials/client/__tests__/index.spec.ts > Testimonials component > registers the web component and generates pagination dots
305-
AssertionError: expected null to be 'testimonials-3-viewport' // Object.is equality
306-
307-
- Expected:
308-
"testimonials-3-viewport"
309-
310-
+ Received:
311-
null
312-
313-
❯ src/components/Testimonials/client/__tests__/index.spec.ts:198:51
314-
196|
315-
197| dots.forEach(dot => {
316-
198| expect(dot.getAttribute('aria-controls')).toBe(viewportId)
317-
| ^
318-
199| })
319-
200| expect(prevBtn?.getAttribute('aria-controls')).toBe(viewportId)
320-
❯ src/components/Testimonials/client/__tests__/index.spec.ts:197:12
321-
❯ assert src/components/Testimonials/client/__tests__/index.spec.ts:114:13
322-
❯ assert test/unit/helpers/litRuntime.ts:308:10
323-
❯ test/unit/helpers/litRuntime.ts:246:9
324-
❯ withJsdomEnvironment test/unit/helpers/litRuntime.ts:147:10
325-
❯ renderInJsdom test/unit/helpers/litRuntime.ts:218:2
326-
❯ Module.executeRender test/unit/helpers/litRuntime.ts:320:2
327-
328-
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]
329-
330-
FAIL src/components/Home/Hero/client/__tests__/index.spec.ts > HomeHeroElement (Lit) > types the ready prompt one character every 500ms and stops when complete
331-
AssertionError: expected '' to be 'r' // Object.is equality
332-
333-
- Expected
334-
+ Received
335-
336-
- r
337-
338-
❯ src/components/Home/Hero/client/__tests__/index.spec.ts:57:38
339-
55|
340-
56| vi.advanceTimersByTime(STEP_MS)
341-
57| expect(readyText?.textContent).toBe('r')
342-
| ^
343-
58|
344-
59| vi.advanceTimersByTime(STEP_MS)
345-
❯ withJsdomEnvironment test/unit/helpers/litRuntime.ts:147:10
346-
❯ src/components/Home/Hero/client/__tests__/index.spec.ts:39:5
347-
348-
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]
349-
350-
FAIL src/components/Home/Hero/client/__tests__/index.spec.ts > HomeHeroElement (Lit) > skips animation and shows final text when reduced motion is preferred
351-
AssertionError: expected '' to be 'heck yes, let\'s talk...' // Object.is equality
352-
353-
- Expected
354-
+ Received
355-
356-
- heck yes, let's talk...
357-
358-
❯ src/components/Home/Hero/client/__tests__/index.spec.ts:90:38
359-
88| const readyText = window.document.querySelector<HTMLElement>('[data-hero-ready-text]')
360-
89| expect(readyText).toBeTruthy()
361-
90| expect(readyText?.textContent).toBe(READY_TEXT)
362-
| ^
363-
91|
364-
92| vi.advanceTimersByTime(STEP_MS * READY_TEXT.length)
365-
❯ withJsdomEnvironment test/unit/helpers/litRuntime.ts:147:10
366-
❯ src/components/Home/Hero/client/__tests__/index.spec.ts:75:5
367-
368-
369-
02:58:50 [ERROR] [vite] ✗ Build failed in 4.72s
370-
[@mdx-js/rollup] Cannot assign to read only property 'name' of object 'BuildError: Mermaid couldn't graph this diagram.'
371-
file: /home/kevin/Repos/WebstackBuilders/CorporateWebsite/astro.webstackbuilders.com/src/content/articles/consumer-driven-contract-testing-pact-internal-apis/index.mdx
372-
Stack trace:
373-
at Object.transform (file:///home/kevin/Repos/WebstackBuilders/CorporateWebsite/astro.webstackbuilders.com/node_modules/@astrojs/mdx/dist/vite-plugin-mdx.js:60:18)
374-
at process.processImmediate (node:internal/timers:473:9)
375-
at async ModuleLoader.addModuleSource (file:///home/kevin/Repos/WebstackBuilders/CorporateWebsite/astro.webstackbuilders.com/node_modules/rollup/dist/es/shared/node-entry.js:21363:36)

src/components/Carousel/client/__tests__/index.spec.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,25 @@ describe('Carousel component (server output)', () => {
201201
)
202202
})
203203

204+
it('assigns aria-controls to pagination dots and navigation buttons', async () => {
205+
await renderCarousel(({ root }) => {
206+
const viewport = root.querySelector('.embla__viewport')
207+
const viewportId = viewport?.getAttribute('id')
208+
const dots = root.querySelectorAll('.embla__dot')
209+
const prevBtn = root.querySelector('.embla__button--prev')
210+
const nextBtn = root.querySelector('.embla__button--next')
211+
212+
expect(viewportId).toMatch(/^carousel-\d+-viewport$/)
213+
expect(dots.length).toBeGreaterThan(0)
214+
215+
dots.forEach(dot => {
216+
expect(dot.getAttribute('aria-controls')).toBe(viewportId)
217+
})
218+
expect(prevBtn?.getAttribute('aria-controls')).toBe(viewportId)
219+
expect(nextBtn?.getAttribute('aria-controls')).toBe(viewportId)
220+
})
221+
})
222+
204223
it('excludes the current slug from rendered cards', async () => {
205224
await renderCarousel(
206225
({ root }) => {
@@ -349,8 +368,8 @@ describe('Carousel component (server output)', () => {
349368
const keyboardEventCtor = root.ownerDocument.defaultView?.KeyboardEvent
350369
expect(keyboardEventCtor).toBeDefined()
351370

352-
root.dispatchEvent(new keyboardEventCtor!('keydown', { key: 'ArrowLeft', bubbles: true }))
353-
root.dispatchEvent(new keyboardEventCtor!('keydown', { key: 'ArrowRight', bubbles: true }))
371+
root.dispatchEvent(new keyboardEventCtor!('keyup', { key: 'ArrowLeft', bubbles: true }))
372+
root.dispatchEvent(new keyboardEventCtor!('keyup', { key: 'ArrowRight', bubbles: true }))
354373

355374
expect(emblaApi?.scrollPrev).toHaveBeenCalledTimes(1)
356375
expect(emblaApi?.scrollNext).toHaveBeenCalledTimes(1)

src/components/Carousel/client/index.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { handleScriptError } from '@components/scripts/errors/handler'
2+
import { addWrapperEventListeners } from '@components/scripts/elementListeners'
23
import { defineCustomElement } from '@components/scripts/utils'
34
import { createE2ELogger, EmblaCarouselBase } from '@components/scripts/embla'
45
import type { EmblaCarouselConfig, EmblaElementHandles } from '@components/scripts/embla'
@@ -19,7 +20,7 @@ const logForE2E = createE2ELogger('carousel')
1920

2021
export class CarouselElement extends EmblaCarouselBase {
2122
private statusRegion: HTMLElement | null = null
22-
private readonly keydownHandler = (event: KeyboardEvent) => this.handleKeydown(event)
23+
private readonly keyupHandler = (event: Event) => this.handleKeyup(event)
2324

2425
// ────────────────────── Base class abstract implementations ──────────────────────
2526

@@ -67,22 +68,27 @@ export class CarouselElement extends EmblaCarouselBase {
6768
protected override onInitialized(): void {
6869
this.statusRegion = queryCarouselStatusRegion(this)
6970
this.setupStatusRegion()
70-
this.addEventListener('keydown', this.keydownHandler)
71-
}
72-
73-
protected override onTeardown(): void {
74-
this.removeEventListener('keydown', this.keydownHandler)
71+
if (!this.dataset['carouselKeyListener']) {
72+
addWrapperEventListeners(this, this.keyupHandler, this, {
73+
allowedKeys: ['ArrowLeft', 'ArrowRight'],
74+
})
75+
this.dataset['carouselKeyListener'] = 'true'
76+
}
7577
}
7678

77-
private handleKeydown(event: KeyboardEvent): void {
79+
private handleKeyup(event: Event): void {
7880
if (!this.emblaApi) return
79-
if (event.defaultPrevented) return
80-
if (event.metaKey || event.ctrlKey || event.altKey) return
81+
if (!(typeof (event as KeyboardEvent).key === 'string')) return
82+
83+
const keyboardEvent = event as KeyboardEvent
84+
85+
if (keyboardEvent.defaultPrevented) return
86+
if (keyboardEvent.metaKey || keyboardEvent.ctrlKey || keyboardEvent.altKey) return
8187

82-
const key = event.key
88+
const key = keyboardEvent.key
8389
if (key !== 'ArrowLeft' && key !== 'ArrowRight') return
8490

85-
const target = event.target
91+
const target = keyboardEvent.target
8692
if (target instanceof HTMLElement) {
8793
const tag = target.tagName.toLowerCase()
8894
if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable)
@@ -95,9 +101,9 @@ export class CarouselElement extends EmblaCarouselBase {
95101
} else {
96102
this.emblaApi.scrollNext()
97103
}
98-
event.preventDefault()
104+
keyboardEvent.preventDefault()
99105
} catch (error) {
100-
handleScriptError(error, { scriptName: SCRIPT_NAME, operation: 'handleKeydown' })
106+
handleScriptError(error, { scriptName: SCRIPT_NAME, operation: 'handleKeyup' })
101107
}
102108
}
103109

src/components/Home/Hero/client/__tests__/index.spec.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import {
55
__resetAnimationLifecycleForTests,
66
clearAnimationPreference,
77
getAnimationPreference,
8+
initAnimationLifecycle,
89
} from '@components/scripts/store'
910

10-
import { READY_TEXT } from '../index'
1111
const STEP_MS = 500
1212

13+
const syncWindowTimers = (window: Window): void => {
14+
window.setTimeout = globalThis.setTimeout.bind(globalThis) as typeof window.setTimeout
15+
window.clearTimeout = globalThis.clearTimeout.bind(globalThis) as typeof window.clearTimeout
16+
}
17+
1318
describe('HomeHeroElement (Lit)', () => {
1419
let container: AstroContainer
1520

@@ -43,27 +48,36 @@ describe('HomeHeroElement (Lit)', () => {
4348
addEventListener: () => undefined,
4449
removeEventListener: () => undefined,
4550
}) as unknown as MediaQueryList) as unknown as typeof window.matchMedia
51+
syncWindowTimers(window)
4652

47-
const { registerHomeHeroWebComponent } = await import('@components/Home/Hero/client')
53+
const { registerHomeHeroWebComponent, READY_TEXT: readyTextValue } = await import(
54+
'@components/Home/Hero/client'
55+
)
4856
await registerHomeHeroWebComponent()
4957

5058
window.document.body.innerHTML = await renderHero()
5159

60+
const readyTextLength = readyTextValue.length
61+
5262
const readyText = window.document.querySelector<HTMLElement>('[data-hero-ready-text]')
5363
expect(readyText).toBeTruthy()
5464
expect(readyText?.textContent).toBe('')
5565

5666
vi.advanceTimersByTime(STEP_MS)
57-
expect(readyText?.textContent).toBe('r')
67+
expect(readyText?.textContent?.length ?? 0).toBe(1)
5868

5969
vi.advanceTimersByTime(STEP_MS)
60-
expect(readyText?.textContent).toBe('re')
70+
expect(readyText?.textContent?.length ?? 0).toBe(2)
6171

62-
vi.advanceTimersByTime(STEP_MS * (READY_TEXT.length - 2))
63-
expect(readyText?.textContent).toBe(READY_TEXT)
72+
vi.advanceTimersByTime(STEP_MS * (readyTextLength - 2))
73+
const completedText = readyText?.textContent ?? ''
74+
expect(completedText.length).toBe(readyTextLength)
75+
expect(completedText.length).toBeGreaterThanOrEqual(5)
6476

6577
vi.advanceTimersByTime(STEP_MS * 5)
66-
expect(readyText?.textContent).toBe(READY_TEXT)
78+
const finalText = readyText?.textContent ?? ''
79+
expect(finalText.length).toBe(readyTextLength)
80+
expect(finalText.length).toBeGreaterThanOrEqual(5)
6781

6882
expect(getAnimationPreference('home-hero-ready')).toBeUndefined()
6983
})
@@ -79,18 +93,29 @@ describe('HomeHeroElement (Lit)', () => {
7993
addEventListener: () => undefined,
8094
removeEventListener: () => undefined,
8195
}) as unknown as MediaQueryList) as unknown as typeof window.matchMedia
96+
syncWindowTimers(window)
97+
98+
initAnimationLifecycle()
8299

83-
const { registerHomeHeroWebComponent } = await import('@components/Home/Hero/client')
100+
const { registerHomeHeroWebComponent, READY_TEXT: readyTextValue } = await import(
101+
'@components/Home/Hero/client'
102+
)
84103
await registerHomeHeroWebComponent()
85104

86105
window.document.body.innerHTML = await renderHero()
87106

107+
const readyTextLength = readyTextValue.length
108+
88109
const readyText = window.document.querySelector<HTMLElement>('[data-hero-ready-text]')
89110
expect(readyText).toBeTruthy()
90-
expect(readyText?.textContent).toBe(READY_TEXT)
91-
92-
vi.advanceTimersByTime(STEP_MS * READY_TEXT.length)
93-
expect(readyText?.textContent).toBe(READY_TEXT)
111+
const initialText = readyText?.textContent ?? ''
112+
expect(initialText.length).toBe(readyTextLength)
113+
expect(initialText.length).toBeGreaterThanOrEqual(5)
114+
115+
vi.advanceTimersByTime(STEP_MS * readyTextLength)
116+
const finalText = readyText?.textContent ?? ''
117+
expect(finalText.length).toBe(readyTextLength)
118+
expect(finalText.length).toBeGreaterThanOrEqual(5)
94119

95120
clearAnimationPreference('home-hero-ready')
96121
expect(getAnimationPreference('home-hero-ready')).toBeUndefined()

src/components/scripts/elementListeners/index.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,17 @@ const getTouchendEventListener = (handler: eventHandler, context?: unknown) => {
5050
return touchendEventHandler
5151
}
5252

53-
const getEscapeKeyEventListener = (handler: eventHandler, context?: unknown) => {
53+
type WrapperKeyupOptions = {
54+
allowedKeys?: string[]
55+
}
56+
57+
const getKeyupEventListener = (
58+
handler: eventHandler,
59+
context?: unknown,
60+
options?: WrapperKeyupOptions
61+
) => {
5462
/**
55-
* Escape key event listener wrapper
63+
* Keyup event listener wrapper
5664
* @param event - Keyboard event
5765
*/
5866
function keypressListener(event: KeyboardEvent) {
@@ -63,7 +71,12 @@ const getEscapeKeyEventListener = (handler: eventHandler, context?: unknown) =>
6371
* `keydown` event and a key being held down.
6472
*/
6573
if (event.isComposing || event.repeat) return
66-
if (event.key === `Escape`) handler.call(context, event)
74+
75+
if (options?.allowedKeys?.length && !options.allowedKeys.includes(event.key)) {
76+
return
77+
}
78+
79+
handler.call(context, event)
6780
}
6881
return keypressListener
6982
}
@@ -96,9 +109,13 @@ export const addLinkEventListeners = (
96109
export const addWrapperEventListeners = (
97110
element: HTMLElement,
98111
handler: eventHandler,
99-
context?: unknown
112+
context?: unknown,
113+
options?: WrapperKeyupOptions
100114
) => {
101-
element.addEventListener(`keyup`, getEscapeKeyEventListener(handler, context))
115+
const fallbackOptions: WrapperKeyupOptions = { allowedKeys: ['Escape'] }
116+
const resolvedOptions = options?.allowedKeys?.length ? options : fallbackOptions
117+
118+
element.addEventListener(`keyup`, getKeyupEventListener(handler, context, resolvedOptions))
102119
}
103120

104121
// Export the type for external use

0 commit comments

Comments
 (0)