Skip to content

Commit 7f4651c

Browse files
committed
Update Carousel component tests to new pattern
1 parent 93adec4 commit 7f4651c

4 files changed

Lines changed: 138 additions & 90 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const sampleCollection = [
2+
{
3+
id: 'article-one',
4+
data: {
5+
title: 'Article One',
6+
description: 'First test entry',
7+
publishDate: '2024-01-01',
8+
featured: true,
9+
icon: '/icons/one.svg',
10+
},
11+
},
12+
{
13+
id: 'article-two',
14+
data: {
15+
title: 'Article Two',
16+
description: 'Second test entry',
17+
publishDate: '2024-02-01',
18+
featured: true,
19+
},
20+
},
21+
{
22+
id: 'article-three',
23+
data: {
24+
title: 'Article Three',
25+
description: 'Third test entry',
26+
publishDate: '2024-03-01',
27+
featured: false,
28+
},
29+
},
30+
{
31+
id: 'article-four',
32+
data: {
33+
title: 'Article Four',
34+
description: 'Fourth test entry',
35+
publishDate: '2024-04-01',
36+
featured: false,
37+
},
38+
},
39+
]
40+
41+
export default sampleCollection
Lines changed: 89 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,79 @@
11
// @vitest-environment node
22

3-
import { describe, it, expect, beforeEach, vi } from 'vitest'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
44
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5-
import { Window } from 'happy-dom'
65
import CarouselComponent from '@components/Carousel/index.astro'
6+
import type { Props as CarouselProps } from '@components/Carousel/index.astro'
7+
import type { CarouselElement } from '@components/Carousel/client'
8+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
9+
import { executeRender } from '@test/unit/helpers/litRuntime'
10+
import sampleCollection from '@components/Carousel/client/__fixtures__/collection.fixture'
711

8-
type CarouselProps = {
9-
title?: string
10-
limit?: number
11-
variant?: 'featured' | 'suggested' | 'random'
12-
currentSlug?: string
13-
type?: 'services' | 'case-studies' | 'articles'
14-
}
15-
16-
const sampleCollection = [
17-
{
18-
id: 'article-one',
19-
data: {
20-
title: 'Article One',
21-
description: 'First test entry',
22-
publishDate: '2024-01-01',
23-
featured: true,
24-
icon: '/icons/one.svg',
25-
},
26-
},
27-
{
28-
id: 'article-two',
29-
data: {
30-
title: 'Article Two',
31-
description: 'Second test entry',
32-
publishDate: '2024-02-01',
33-
featured: true,
34-
},
35-
},
36-
{
37-
id: 'article-three',
38-
data: {
39-
title: 'Article Three',
40-
description: 'Third test entry',
41-
publishDate: '2024-03-01',
42-
featured: false,
43-
},
44-
},
45-
{
46-
id: 'article-four',
47-
data: {
48-
title: 'Article Four',
49-
description: 'Fourth test entry',
50-
publishDate: '2024-04-01',
51-
featured: false,
52-
},
53-
},
54-
]
12+
type CarouselComponentModule = WebComponentModule<CarouselElement>
13+
type ConcreteCarouselProps = Required<CarouselProps<'articles'>>
5514

5615
vi.mock('astro:content', () => ({
5716
getCollection: vi.fn(async () => sampleCollection),
5817
}))
5918

60-
const baseProps: Required<CarouselProps> = {
19+
vi.mock('embla-carousel', () => {
20+
const createEmbla = vi.fn(() => ({
21+
canScrollPrev: vi.fn(() => false),
22+
canScrollNext: vi.fn(() => false),
23+
scrollPrev: vi.fn(),
24+
scrollNext: vi.fn(),
25+
on: vi.fn(() => createEmbla.mock.results.at(-1)?.value ?? {}),
26+
off: vi.fn(() => createEmbla.mock.results.at(-1)?.value ?? {}),
27+
destroy: vi.fn(),
28+
scrollSnapList: vi.fn(() => [0, 1, 2]),
29+
selectedScrollSnap: vi.fn(() => 0),
30+
scrollTo: vi.fn(),
31+
}))
32+
33+
return {
34+
default: createEmbla,
35+
}
36+
})
37+
38+
vi.mock('embla-carousel-autoplay', () => ({
39+
__esModule: true,
40+
default: vi.fn(() => ({
41+
play: vi.fn(),
42+
stop: vi.fn(),
43+
})),
44+
}))
45+
46+
const defaultCarouselProps: ConcreteCarouselProps = {
6147
title: 'Featured Articles',
6248
limit: 3,
6349
variant: 'featured',
6450
currentSlug: 'article-two',
6551
type: 'articles',
6652
}
6753

68-
const renderCarousel = async (props?: Partial<CarouselProps>) => {
54+
/**
55+
* Renders the Carousel component in a shared JSDOM instance and exposes the hydrated element.
56+
*/
57+
const renderCarousel = async (
58+
assertion: (_context: { root: CarouselElement }) => Promise<void> | void,
59+
props: Partial<ConcreteCarouselProps> = {},
60+
) => {
6961
const container = await AstroContainer.create()
70-
const html = await container.renderToString(CarouselComponent, {
71-
props: { ...baseProps, ...props },
72-
})
73-
74-
const window = new Window()
75-
const domParser = new window.DOMParser()
76-
const document = domParser.parseFromString(html, 'text/html')
7762

78-
return { html, document }
63+
await executeRender<CarouselComponentModule>({
64+
container,
65+
component: CarouselComponent,
66+
moduleSpecifier: '@components/Carousel/client/index',
67+
args: {
68+
props: {
69+
...defaultCarouselProps,
70+
...props,
71+
},
72+
},
73+
assert: async ({ element }) => {
74+
await assertion({ root: element })
75+
},
76+
})
7977
}
8078

8179
describe('Carousel component (server output)', () => {
@@ -84,48 +82,52 @@ describe('Carousel component (server output)', () => {
8482
})
8583

8684
it('renders the provided title and respects the requested limit', async () => {
87-
const { document } = await renderCarousel({ title: 'Latest Reads', limit: 2, currentSlug: 'article-four' })
88-
89-
const heading = document.querySelector('h2')
90-
const slides = document.querySelectorAll('[data-carousel-slide]')
85+
await renderCarousel(({ root }) => {
86+
const heading = root.querySelector('h2')
87+
const slides = root.querySelectorAll('[data-carousel-slide]')
9188

92-
expect(heading?.textContent).toBe('Latest Reads')
93-
expect(slides).toHaveLength(2)
89+
expect(heading?.textContent).toBe('Latest Reads')
90+
expect(slides).toHaveLength(2)
91+
}, { title: 'Latest Reads', limit: 2, currentSlug: 'article-four' })
9492
})
9593

9694
it('excludes the current slug from rendered cards', async () => {
97-
const { document } = await renderCarousel({ currentSlug: 'article-one' })
98-
const links = Array.from(document.querySelectorAll('a[href^="/articles/"]'))
99-
const hrefs = links.map(link => link.getAttribute('href'))
95+
await renderCarousel(({ root }) => {
96+
const links = Array.from(root.querySelectorAll('a[href^="/articles/"]'))
97+
const hrefs = links.map(link => link.getAttribute('href'))
10098

101-
expect(hrefs).not.toContain('/articles/article-one')
99+
expect(hrefs).not.toContain('/articles/article-one')
100+
}, { currentSlug: 'article-one' })
102101
})
103102

104103
it('renders featured items with icons when available', async () => {
105-
const { document } = await renderCarousel()
106-
const cardWithIcon = document.querySelector('[data-carousel-slide] img')
104+
await renderCarousel(({ root }) => {
105+
const cardWithIcon = root.querySelector('[data-carousel-slide] img')
107106

108-
expect(cardWithIcon).toBeTruthy()
109-
expect(cardWithIcon?.getAttribute('src')).toBe('/icons/one.svg')
107+
expect(cardWithIcon).toBeTruthy()
108+
expect(cardWithIcon?.getAttribute('src')).toBe('/icons/one.svg')
109+
})
110110
})
111111

112112
it('orders suggested items by publish date when variant is suggested', async () => {
113-
const { document } = await renderCarousel({ variant: 'suggested', limit: 3 })
114-
const cardTitles = Array.from(document.querySelectorAll('[data-carousel-slide] h3')).map(title =>
115-
title.textContent?.trim(),
116-
)
117-
118-
expect(cardTitles[0]).toBe('Article Four')
119-
expect(cardTitles[1]).toBe('Article Three')
113+
await renderCarousel(({ root }) => {
114+
const cardTitles = Array.from(root.querySelectorAll('[data-carousel-slide] h3')).map(title =>
115+
title.textContent?.trim(),
116+
)
117+
118+
expect(cardTitles[0]).toBe('Article Four')
119+
expect(cardTitles[1]).toBe('Article Three')
120+
}, { variant: 'suggested', limit: 3 })
120121
})
121122

122123
it('renders randomized order when variant is random', async () => {
123-
const { document } = await renderCarousel({ variant: 'random', limit: 3 })
124-
const cardTitles = Array.from(document.querySelectorAll('[data-carousel-slide] h3')).map(title =>
125-
title.textContent?.trim(),
126-
)
127-
128-
expect(cardTitles).toHaveLength(3)
129-
expect(new Set(cardTitles)).toEqual(new Set(['Article Four', 'Article Three', 'Article One']))
124+
await renderCarousel(({ root }) => {
125+
const cardTitles = Array.from(root.querySelectorAll('[data-carousel-slide] h3')).map(title =>
126+
title.textContent?.trim(),
127+
)
128+
129+
expect(cardTitles).toHaveLength(3)
130+
expect(new Set(cardTitles)).toEqual(new Set(['Article Four', 'Article Three', 'Article One']))
131+
}, { variant: 'random', limit: 3 })
130132
})
131133
})

‎src/components/Carousel/client/index.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { addButtonEventListeners } from '@components/scripts/elementListeners'
44
import { addScriptBreadcrumb, ClientScriptError } from '@components/scripts/errors'
55
import { handleScriptError } from '@components/scripts/errors/handler'
66
import { defineCustomElement } from '@components/scripts/utils'
7+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
78

89
const SCRIPT_NAME = 'CarouselElement'
910

@@ -302,3 +303,9 @@ export const registerCarouselWebComponent = (tagName = 'carousel-slider') => {
302303
if (typeof window === 'undefined') return
303304
defineCustomElement(tagName, CarouselElement)
304305
}
306+
307+
export const webComponentModule: WebComponentModule<CarouselElement> = {
308+
registeredName: 'carousel-slider',
309+
componentCtor: CarouselElement,
310+
registerWebComponent: registerCarouselWebComponent,
311+
}

‎src/components/scripts/@types/webComponentModule.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import type { LitElement } from 'lit'
2-
3-
export interface WebComponentModule<TElement extends LitElement = LitElement> {
1+
export interface WebComponentModule<TElement extends HTMLElement = HTMLElement> {
42
registeredName: string
53
componentCtor: CustomElementConstructor & { prototype: TElement }
64
registerWebComponent: (_tagName?: string) => Promise<void> | void

0 commit comments

Comments
 (0)