import * as React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { axe } from 'vitest-axe'
import { CopyButton } from './copy-button'
// userEvent.setup() installs its own navigator.clipboard stub, so the spy
// must be created after setup() - a beforeEach mock would get replaced.
function setup() {
const user = userEvent.setup()
const writeText = vi.spyOn(navigator.clipboard, 'writeText')
return { user, writeText }
}
describe('CopyButton', () => {
it('renders a button with an accessible name', () => {
render()
expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument()
})
it('renders a visible label alongside the icon when children are passed', () => {
render(Copy SVG)
expect(screen.getByText('Copy SVG')).toBeInTheDocument()
})
it('swaps a string label to the copied label on success', async () => {
const { user } = setup()
render(
Copy SVG
,
)
await user.click(screen.getByRole('button'))
await waitFor(() => expect(screen.queryByText('Copy SVG')).not.toBeInTheDocument())
// Both the visible label and the sr-only status announce "Copied".
expect(screen.getAllByText('Copied').length).toBeGreaterThanOrEqual(1)
})
it('copies the string value on click', async () => {
const { user, writeText } = setup()
const onCopy = vi.fn()
render()
await user.click(screen.getByRole('button', { name: 'Copy' }))
expect(writeText).toHaveBeenCalledWith('hello')
expect(onCopy).toHaveBeenCalledWith('hello')
})
it('resolves a function value, including async ones', async () => {
const { user, writeText } = setup()
render( 'computed'} />)
await user.click(screen.getByRole('button'))
expect(writeText).toHaveBeenCalledWith('computed')
})
it('copies the value of a referenced input', async () => {
const { user, writeText } = setup()
function Demo() {
const inputRef = React.useRef(null)
return (
<>
>
)
}
render()
await user.click(screen.getByRole('button', { name: 'Copy' }))
expect(writeText).toHaveBeenCalledWith('from input')
})
it('copies the textContent of a referenced element', async () => {
const { user, writeText } = setup()
function Demo() {
const preRef = React.useRef(null)
return (
<>
npm install appica
>
)
}
render()
await user.click(screen.getByRole('button', { name: 'Copy' }))
expect(writeText).toHaveBeenCalledWith('npm install appica')
})
it('switches to the copied state and reverts after the timeout', async () => {
const { user } = setup()
render()
const button = screen.getByRole('button', { name: 'Copy' })
await user.click(button)
expect(button).toHaveAccessibleName('Copied')
await waitFor(() => expect(button).toHaveAccessibleName('Copy'))
})
it('uses custom labels for the accessible name', async () => {
const { user } = setup()
render()
const button = screen.getByRole('button', { name: 'Copy code' })
await user.click(button)
expect(button).toHaveAccessibleName('Code copied')
})
it('reports failures via onCopyError and stays idle', async () => {
const { user, writeText } = setup()
const error = new Error('denied')
writeText.mockRejectedValueOnce(error)
const onCopy = vi.fn()
const onCopyError = vi.fn()
render()
const button = screen.getByRole('button', { name: 'Copy' })
await user.click(button)
expect(onCopyError).toHaveBeenCalledWith(error)
expect(onCopy).not.toHaveBeenCalled()
expect(button).toHaveAccessibleName('Copy')
})
it('forwards button props', () => {
render()
const button = screen.getByRole('button')
expect(button.className).toContain('custom-class')
expect(button).toHaveAttribute('data-disabled')
})
it('has no accessibility violations', async () => {
const { container } = render()
expect(await axe(container)).toHaveNoViolations()
})
})