Skip to content

Instantly share code, notes, and snippets.

@devinschumacher
Last active December 10, 2024 22:07
Show Gist options
  • Select an option

  • Save devinschumacher/6a7a30a14ac47eb86f6f85065cc60b08 to your computer and use it in GitHub Desktop.

Select an option

Save devinschumacher/6a7a30a14ac47eb86f6f85065cc60b08 to your computer and use it in GitHub Desktop.
Software Testing Prompts
tags
test
tests

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:

  1. Use renderSuspended from @nuxt/test-utils/runtime:

    • Utilize renderSuspended to 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.
  2. Employ Testing Library for Assertions and Interactions:

    • Use @testing-library/vue in conjunction with renderSuspended to interact with the rendered component.
    • Import utilities like screen, within, and userEvent from Testing Library for querying DOM elements and simulating user events.
    • Write assertions using Jest and @testing-library/jest-dom for improved readability and better assertion messages.
  3. 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, and getByAltText to 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 it or test blocks 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'.
  4. Structure Your Test File Appropriately:

    • Begin by importing necessary modules and the component to test.
    • Use describe blocks to group related tests.
    • Use beforeEach or afterEach for any setup or teardown logic if necessary.
    • Ensure each test is independent and can run in isolation.
  5. Handle Asynchronous Operations Properly:

    • Use async/await to handle any asynchronous behavior in your component.
    • Utilize findBy queries when waiting for elements that appear asynchronously.
    • Use waitFor when you need to wait for certain conditions before making assertions.

Examples:

Below are examples of components and their corresponding tests that follow the best practices mentioned above.


Example 1: TransitionAnimation Component

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',
  })
})

Example 2: LinkButton Component

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
})

Example 3: ArticleLink Component

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()
})

Example 4:

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 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.

Component code:

// components/ComponentName.vue
@devinschumacher
Copy link
Copy Markdown
Author

Example tests

More examples of tests that work.

These are jest syntax

import { expect, it, describe } from 'vitest';
import NotFoundPage from '@/pages/404.vue';
import { mount } from '@vue/test-utils';

// Render test
describe('NotFoundPage', () => {
  it('renders', () => {
    const wrapper = mount(NotFoundPage);
    expect(wrapper.exists()).toBe(true);
  });
})


// Access component data (text)
describe('NotFoundPage', () => {
  it('displays heading 1', () => {
    const wrapper = mount(NotFoundPage);
    const message = wrapper.find('h1').text();
    expect(message).toBe('404');
  });
});

test user interaction and assert on result

import { expect, it, describe } from 'vitest';
import MyComponent from '@/components/MyComponent.vue';
import { mount } from '@vue/test-utils';


describe('MyComponent', () => {
  it('increments count when button is clicked', async () => {
  const wrapper = mount(MyComponent);

  // Find the button and trigger a click
  const button = wrapper.find('button');
  await button.trigger('click');

  // Find the element displaying the count by selecting the element by it's class
  const countDisplay = wrapper.find('.count-display');

  // Assert the count value from the element's text
  expect(countDisplay.text()).toBe('Count: 1');
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment