// @vitest-environment jsdom
/**
* What jsdom can see of the ScrollArea page: the content renders inside the
* region, and the region is Base UI's. Whether the bar takes width, and when
* it shows, is measured in Chrome (2026-09-08: the viewport keeps its full
* width with 40 rows overflowing; the native bar is hidden).
*/
import { createRef } from 'react'
import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { ScrollArea } from './ScrollArea'
afterEach(cleanup)
describe('ScrollArea', () => {
it('draws the content inside a viewport of its own', () => {
const { container } = render(
Inside
,
)
const inside = screen.getByText('Inside')
expect(container.firstElementChild).not.toBe(inside)
expect(container.firstElementChild!.contains(inside)).toBe(true)
expect(container.firstElementChild!.className).toContain('h-40')
})
it('puts each class prop on its own box: the region, the viewport, the content', () => {
const { container } = render(
Inside
,
)
const region = container.firstElementChild!
const content = screen.getByText('Inside').parentElement!
const viewport = content.parentElement!
expect(region.className).toContain('h-40')
expect(viewport.className).toContain('max-h-20')
expect(content.className).toContain('p-2')
expect(viewport.parentElement).toBe(region)
})
it('keeps a vertical region no wider than itself, and lets a sideways one grow', () => {
const v = render(Inside
)
expect((screen.getByText('Inside').parentElement as HTMLElement).style.minWidth).toBe('0px')
cleanup()
render(Inside
)
expect((screen.getByText('Inside').parentElement as HTMLElement).style.minWidth).toBe('fit-content')
void v
})
})
describe('ScrollArea and the page behind it', () => {
// Measured in Chrome (2026-09-09, PLAN Finding 40): with an unconditional
// `overscroll-contain` the page under a sideways table moved 0px on a
// wheel down, 446px without it; with these two classes 400px, and the
// table 0. jsdom cannot scroll; it pins that the contain is per axis and
// waits for Base UI's overflow attribute rather than being unconditional.
it('contains the wheel per axis, and only while that axis overflows', () => {
render(Inside
)
const viewport = screen.getByText('Inside').parentElement!.parentElement!
expect(viewport.className).toContain('data-[has-overflow-x]:overscroll-x-contain')
expect(viewport.className).toContain('data-[has-overflow-y]:overscroll-y-contain')
expect(viewport.className).not.toMatch(/(^|\s)overscroll-contain(\s|$)/)
})
})
/**
* The viewport is the box a caller reads and drives (0.12.2, ADOPTION B20).
* A conversation arrives at its newest message and jumps to the bottom on a
* reply; neither is something the region can do for the caller, and neither is
* possible without the box.
*/
describe('ScrollArea, the viewport it hands back', () => {
it('gives the caller the box that scrolls, and reports its scrolling', () => {
const ref = createRef()
let scrolled = 0
render(
{ scrolled += 1 }}>
Inside
,
)
// The box the caller gets is the one the content sits in, not the region.
expect(ref.current).not.toBe(null)
expect(ref.current!.contains(screen.getByText('Inside'))).toBe(true)
expect(ref.current!.className).toContain('overflow')
fireEvent.scroll(ref.current!)
expect(scrolled).toBe(1)
})
})