import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { I18nProvider } from '@/lib/i18n-context'
import { CodeBlock } from '@/components/CodeBlock'
afterEach(() => cleanup())
// In jsdom the async shiki highlighter never resolves, so these tests
// exercise the plain-text fallback (code split on newlines), which is the
// same per-line row structure the highlighted path produces.
describe('CodeBlock', () => {
beforeEach(() => {
window.localStorage.clear()
})
it('renders a header label and truncation badge for long content', () => {
const longCode = Array.from({ length: 40 }, (_, index) => `line ${index + 1}`).join('\n')
render(
element for preformatted semantics', () => {
const { container } = render(
)
const pre = container.querySelector('pre.shiki')
expect(pre).not.toBeNull()
expect(pre?.querySelector('[data-line-number]')).not.toBeNull()
expect(pre?.querySelector('[data-code-cell]')).not.toBeNull()
})
it('defaults to wrap off: line-number column present, horizontal scroll container, no wrapping', () => {
const { container } = render(
)
expect(container.querySelector('[data-line-number]')).not.toBeNull()
expect(container.querySelector('.overflow-x-auto')).not.toBeNull()
const codeCell = container.querySelector('[data-code-cell]') as HTMLElement | null
expect(codeCell?.style.whiteSpace).not.toBe('pre-wrap')
})
it('toggling wrap on keeps the line-number column and wraps the code cells (no horizontal scroll)', () => {
// Select the toggle by its aria-pressed state, not its localized title.
const { container } = render(
)
fireEvent.click(screen.getByRole('button', { pressed: false }))
// Line numbers stay (this is the whole point of the per-line layout).
expect(container.querySelectorAll('[data-line-number]')).toHaveLength(2)
expect(container.querySelector('.overflow-x-auto')).toBeNull()
const codeCell = container.querySelector('[data-code-cell]') as HTMLElement | null
expect(codeCell?.style.whiteSpace).toBe('pre-wrap')
expect(screen.getByRole('button', { pressed: true })).toBeInTheDocument()
})
it('reads the persisted wrap preference on mount and still keeps line numbers', () => {
window.localStorage.setItem('hapi-code-wrap', '1')
const { container } = render(
)
expect(container.querySelectorAll('[data-line-number]')).toHaveLength(2)
const codeCell = container.querySelector('[data-code-cell]') as HTMLElement | null
expect(codeCell?.style.whiteSpace).toBe('pre-wrap')
expect(screen.getByRole('button', { pressed: true })).toBeInTheDocument()
})
it('renders the plain-text fallback as per-line rows when highlighting is unavailable', () => {
const { container } = render(
)
const codeCells = container.querySelectorAll('[data-code-cell]')
expect(codeCells).toHaveLength(2)
expect(codeCells[0]).toHaveTextContent('plain one')
expect(codeCells[1]).toHaveTextContent('plain two')
expect(container.querySelectorAll('[data-line-number]')).toHaveLength(2)
})
it('renders the wrap toggle button by default', () => {
render(
)
expect(screen.getByRole('button', { pressed: false })).toBeInTheDocument()
})
it('omits the wrap toggle button when showWrapToggle is false', () => {
// The wrap toggle is a