| tags |
|
|---|
-
-
Save devinschumacher/6a7a30a14ac47eb86f6f85065cc60b08 to your computer and use it in GitHub Desktop.
Prompt for Writing Nuxt Component Tests
You are provided with a Nuxt component code. Your task is to write comprehensive and high-quality tests for this component using renderSuspended from @nuxt/test-utils/runtime and @testing-library/vue, following best practices.
Requirements:
-
Use
renderSuspendedfrom@nuxt/test-utils/runtime:- Utilize
renderSuspendedto render the component within the Nuxt testing environment. - This ensures the component is tested in an environment similar to production, with access to Nuxt-specific features.
- Utilize
-
Employ Testing Library for Assertions and Interactions:
- Use
@testing-library/vuein conjunction withrenderSuspendedto interact with the rendered component. - Import utilities like
screen,within, anduserEventfrom Testing Library for querying DOM elements and simulating user events. - Write assertions using Jest and
@testing-library/jest-domfor improved readability and better assertion messages.
- Use
-
Adhere to Testing Best Practices:
-
Focus on User Experience and Accessibility:
- Tests should reflect how the end-user interacts with the component.
- Use queries like
getByRole,getByLabelText,getByPlaceholderText, andgetByAltTextto find elements, as these are more robust and reflect accessibility best practices. - Avoid overusing
getByText; use it when other queries are not suitable.
-
Avoid Testing Implementation Details:
- Do not assert on class names, styles, or internal component state.
- Do not rely on specific HTML tags unless they are semantically significant (e.g., headings, lists).
- Focus on the component's behavior and output from the user's perspective.
-
Write Focused and Atomic Tests:
- Each test should focus on a single aspect of the component's behavior.
- Avoid combining multiple assertions that test different functionalities into one test.
- Use separate
itortestblocks for different features or behaviors.
-
Avoid Hardcoding Specific Data:
- Do not hardcode specific values like company names, addresses, or other content that may change.
- Use placeholder text or variables to represent such data.
- If necessary, use regular expressions or partial matches to make the tests flexible.
-
Use Descriptive Test Names:
- Clearly state what each test is verifying.
- Example:
'should render the primary button with correct attributes'.
-
-
Structure Your Test File Appropriately:
- Begin by importing necessary modules and the component to test.
- Use
describeblocks to group related tests. - Use
beforeEachorafterEachfor any setup or teardown logic if necessary. - Ensure each test is independent and can run in isolation.
-
Handle Asynchronous Operations Properly:
- Use
async/awaitto handle any asynchronous behavior in your component. - Utilize
findByqueries when waiting for elements that appear asynchronously. - Use
waitForwhen you need to wait for certain conditions before making assertions.
- Use
Examples:
Below are examples of components and their corresponding tests that follow the best practices mentioned above.
Component:
<template>
<div
ref="target"
:style="targetIsVisible ? props : { opacity: 0 }"
:class="props.class"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { useElementVisibility } from '@vueuse/core'
type TimeUnit = 'ms' | 's'
export interface AnimationProps {
animationDelay?: `${number}${TimeUnit}`
animationDuration: `${number}${TimeUnit}`
animationFillMode?: 'backwards' | 'both' | 'forwards' | 'none'
animationName:
| 'fade-down'
| 'fade-in'
| 'fade-left'
| 'fade-up'
| 'scale-in'
| 'wiggle'
animationTiming?: 'ease-in' | 'ease-in-out' | 'ease-out' | 'linear'
class?: string
}
const props = withDefaults(defineProps<AnimationProps>(), {
animationDelay: undefined,
animationDuration: '1s',
animationFillMode: 'backwards',
animationName: 'fade-in',
animationTiming: 'ease-in',
class: '',
})
const target = ref(null)
const targetIsVisible = useElementVisibility(target, { threshold: 0.05 })
</script>Test:
import { mockNuxtImport, renderSuspended } from '@nuxt/test-utils/runtime'
import { screen } from '@testing-library/vue'
import { beforeEach, expect, it, vi } from 'vitest'
import TransitionAnimation from '~/components/base/transition-animation.vue'
const { useElementVisibility } = vi.hoisted(() => {
return {
useElementVisibility: vi.fn().mockImplementation(() => {
return { value: false }
}),
}
})
mockNuxtImport('useElementVisibility', () => {
return useElementVisibility
})
beforeEach(() => {
useElementVisibility.mockRestore()
})
it('should render content with opacity 0 when not visible', async () => {
await renderSuspended(TransitionAnimation, {
props: {
animationDuration: '1s',
animationName: 'scale-in',
},
slots: {
default: () => 'Content',
},
})
const content = screen.getByText('Content')
expect(content).toHaveStyle('opacity: 0')
})
it('should render content with animation styles when visible', async () => {
useElementVisibility.mockImplementation(() => {
return { value: true }
})
await renderSuspended(TransitionAnimation, {
props: {
animationDuration: '0s',
animationName: 'scale-in',
},
slots: {
default: () => 'Content',
},
})
const content = screen.getByText('Content')
expect(content).toHaveStyle({
'animation-duration': '0s',
'animation-fill-mode': 'backwards',
'animation-name': 'scale-in',
})
})Component:
<template>
<NuxtLink
:class="[
variant === 'primary' && $style.primary,
variant === 'secondary' && $style.secondary,
$style.button,
]"
v-bind="nuxtLinkProps"
>
<slot />
</NuxtLink>
</template>
<script setup lang="ts">
import type { NuxtLinkProps } from '#app'
interface ButtonProps extends NuxtLinkProps {
variant?: 'primary' | 'secondary'
download?: boolean
}
const { download, to = '/', variant, prefetch = true } = defineProps<ButtonProps>()
const nuxtLinkProps = to.toString().startsWith('/')
? { download, to, variant, prefetch }
: {
download,
to,
variant,
prefetch,
external: true,
target: '_blank',
}
</script>
<style module>
.button {
/* Styles */
}
.primary {
/* Styles */
}
.secondary {
/* Styles */
}
</style>Test:
import { renderSuspended } from '@nuxt/test-utils/runtime'
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import LinkButton from '~/components/base/link-button.vue'
import GitHub from '~/components/svg/github.vue'
it('should render external link with correct attributes', async () => {
await renderSuspended(LinkButton, {
props: {
to: 'https://example.com',
},
slots: {
default: () => 'Visit Example',
},
})
const link = screen.getByRole('link', { name: 'Visit Example' })
expect(link).toHaveAttribute('href', 'https://example.com')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})
it('should render slot content with SVG icon', async () => {
await renderSuspended(LinkButton, {
props: {
to: 'https://github.com',
},
slots: {
default: GitHub,
},
})
const icon = screen.getByTitle('GitHub icon')
expect(icon).toBeInTheDocument()
})
it('should apply primary variant classes', async () => {
await renderSuspended(LinkButton, {
props: {
to: '/',
variant: 'primary',
},
slots: {
default: () => 'Home',
},
})
const link = screen.getByRole('link', { name: 'Home' })
expect(link).toHaveClass('button')
// Note: Avoid asserting on specific class names unless necessary
})
it('should apply secondary variant classes', async () => {
await renderSuspended(LinkButton, {
props: {
to: '/',
variant: 'secondary',
},
slots: {
default: () => 'Home',
},
})
const link = screen.getByRole('link', { name: 'Home' })
expect(link).toHaveClass('button')
// Note: Avoid asserting on specific class names unless necessary
})Component:
<template>
<NuxtLink :to="href" target="_blank" :class="$style.link">
<slot />
</NuxtLink>
</template>
<script lang="ts" setup>
const { href } = defineProps<{ href: string }>()
</script>
<style module>
.link {
/* Styles */
}
</style>Test:
import { renderSuspended } from '@nuxt/test-utils/runtime'
import { screen } from '@testing-library/vue'
import { expect, it } from 'vitest'
import ArticleLink from '~/components/base/article-link.vue'
it('should render link with target="_blank" and correct href', async () => {
await renderSuspended(ArticleLink, { props: { href: 'https://example.com' } })
const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', 'https://example.com')
expect(link).toHaveAttribute('target', '_blank')
})
it('should render internal link correctly', async () => {
await renderSuspended(ArticleLink, { props: { href: '/internal-page' } })
const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', '/internal-page')
})
it('should render slot content as link text', async () => {
await renderSuspended(ArticleLink, {
props: { href: '/page' },
slots: { default: () => 'Read More' },
})
const link = screen.getByRole('link', { name: 'Read More' })
expect(link).toBeInTheDocument()
})Page:
<!-- pages/index.vue -->
<template>
<div class="container py-20">
<!-- hero -->
<h1>DAFT FM</h1>
<!-- link hub: songs -->
<h2 class="mt-20">Songs</h2>
<div class="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
<div v-for="song in songsData?.songs" :key="song.id">
<nuxt-link :to="`/song/${song.slug}`" aria-label="song name">
{{ song.name }} -
<span v-for="artist in song.artists" :key="artist.slug">
{{ artist.creditName }}
<span v-if="artist.joinPhrase">{{ artist.joinPhrase }}</span>
</span>
</nuxt-link>
</div>
</div>
<!-- link hub: artists -->
<h2 class="mt-20">Artists</h2>
<div
class="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<div v-for="artist in artistsData?.artists" :key="artist.id">
<span>
<nuxt-link :to="`/artist/${artist.slug}`" aria-label="artist name">
{{ artist.name }}
</nuxt-link>
</span>
</div>
</div>
<!-- link hub: albums -->
<h2 class="mt-20">Albums</h2>
<div class="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
<div v-for="album in albumsData?.albums" :key="album.id">
<nuxt-link :to="`/album/${album.slug}`" aria-label="album name">
{{ album.name }} -
<span v-for="artist in album.artists" :key="artist.slug">
{{ artist.creditName }}
<span v-if="artist.joinPhrase">{{ artist.joinPhrase }}</span>
</span>
</nuxt-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const runtimeConfig = useRuntimeConfig()
const { data: songsData } = await useFetch(
`${runtimeConfig.public.apiUrl}/songs`,
{
lazy: true
}
)
const { data: artistsData } = await useFetch(
`${runtimeConfig.public.apiUrl}/artists`,
{
lazy: true
}
)
const { data: albumsData } = await useFetch(
`${runtimeConfig.public.apiUrl}/albums`,
{
lazy: true
}
)
</script>
Test:
// index.test.ts
import { renderSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime'
import { screen, within } from '@testing-library/vue'
import { expect, it, vi } from 'vitest'
import { ref } from 'vue'
import IndexPage from '@/pages/index.vue'
const mockSongsData = {
songs: [
{
id: 1,
slug: 'song-1',
name: 'Song 1',
artists: [
{ slug: 'artist-1', creditName: 'Artist 1', joinPhrase: ' & ' },
{ slug: 'artist-2', creditName: 'Artist 2', joinPhrase: null }
]
},
{
id: 2,
slug: 'song-2',
name: 'Song 2',
artists: [{ slug: 'artist-3', creditName: 'Artist 3', joinPhrase: null }]
}
]
}
const mockArtistsData = {
artists: [
{ id: 1, slug: 'artist-1', name: 'Artist 1' },
{ id: 2, slug: 'artist-2', name: 'Artist 2' },
{ id: 3, slug: 'artist-3', name: 'Artist 3' }
]
}
const mockAlbumsData = {
albums: [
{
id: 1,
slug: 'album-1',
name: 'Album 1',
artists: [{ slug: 'artist-1', creditName: 'Artist 1', joinPhrase: null }]
},
{
id: 2,
slug: 'album-2',
name: 'Album 2',
artists: [{ slug: 'artist-2', creditName: 'Artist 2', joinPhrase: null }]
}
]
}
const { useFetchMock } = vi.hoisted(() => {
return {
useFetchMock: vi.fn(async (url) => {
let data
if (url.endsWith('/songs')) {
data = ref(mockSongsData)
} else if (url.endsWith('/artists')) {
data = ref(mockArtistsData)
} else if (url.endsWith('/albums')) {
data = ref(mockAlbumsData)
} else {
data = ref(null)
}
return { data }
})
}
})
mockNuxtImport('useFetch', () => {
return useFetchMock
})
describe('IndexPage', () => {
it('should render the hero heading', async () => {
await renderSuspended(IndexPage)
const heading = screen.getByRole('heading', { level: 1, name: 'DAFT FM' })
expect(heading).toBeInTheDocument()
})
it('should render the Songs section with correct song links', async () => {
await renderSuspended(IndexPage)
const songsHeading = screen.getByRole('heading', {
level: 2,
name: 'Songs'
})
expect(songsHeading).toBeInTheDocument()
const songsContainer = songsHeading.nextElementSibling
const { getAllByRole } = within(songsContainer)
const songLinks = getAllByRole('link')
expect(songLinks).toHaveLength(mockSongsData.songs.length)
mockSongsData.songs.forEach((song, index) => {
const link = songLinks[index]
expect(link).toHaveAttribute('href', `/song/${song.slug}`)
expect(link).toHaveTextContent(song.name)
song.artists.forEach((artist) => {
expect(link).toHaveTextContent(artist.creditName)
})
})
})
it('should render the Artists section with correct artist links', async () => {
await renderSuspended(IndexPage)
const artistsHeading = screen.getByRole('heading', {
level: 2,
name: 'Artists'
})
expect(artistsHeading).toBeInTheDocument()
const artistsContainer = artistsHeading.nextElementSibling
const { getAllByRole } = within(artistsContainer)
const artistLinks = getAllByRole('link')
expect(artistLinks).toHaveLength(mockArtistsData.artists.length)
mockArtistsData.artists.forEach((artist, index) => {
const link = artistLinks[index]
expect(link).toHaveAttribute('href', `/artist/${artist.slug}`)
expect(link).toHaveTextContent(artist.name)
})
})
it('should render the Albums section with correct album links', async () => {
await renderSuspended(IndexPage)
const albumsHeading = screen.getByRole('heading', {
level: 2,
name: 'Albums'
})
expect(albumsHeading).toBeInTheDocument()
const albumsContainer = albumsHeading.nextElementSibling
const { getAllByRole } = within(albumsContainer)
const albumLinks = getAllByRole('link')
expect(albumLinks).toHaveLength(mockAlbumsData.albums.length)
mockAlbumsData.albums.forEach((album, index) => {
const link = albumLinks[index]
expect(link).toHaveAttribute('href', `/album/${album.slug}`)
expect(link).toHaveTextContent(album.name)
album.artists.forEach((artist) => {
expect(link).toHaveTextContent(artist.creditName)
})
})
})
})
Additional Guidelines:
-
Use Testing Library's Queries Effectively:
- Prefer queries that target elements by their role and accessible name.
- Example:
getByRole('button', { name: /submit/i }).
-
Avoid Using Class Names or Styles in Tests:
- Do not use assertions like
expect(element).toHaveClass('some-class'). - Class names and styles are implementation details that may change.
- Do not use assertions like
-
Do Not Use Custom Query Functions Based on Implementation Details:
- Do not write custom functions in queries that rely on specific tag names or class lists.
- Stick to the standard queries provided by Testing Library.
-
Focus on User-Facing Behavior and Accessibility:
- Ensure that interactive elements have appropriate ARIA roles and accessible names.
- Verify that the component behaves correctly in response to user interactions.
Deliverables:
- Provide the complete test code for the given Nuxt component.
- Ensure the code is properly formatted and follows the best practices outlined above.
- Tests should be clear, maintainable, and focused on user-facing behavior.
- Do not include any assertions on class names, styles, or specific HTML tags unless they are critical for accessibility or user experience.
- Avoid combining multiple functionalities into a single test; keep tests focused and atomic.
By following this prompt and referring to the examples provided, you should be able to write high-quality tests that focus on the component's behavior from the user's perspective, using best practices, and without relying on implementation details.
// components/ComponentName.vue
Example tests
More examples of tests that work.
These are jest syntax
test user interaction and assert on result