import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { WidgetNote } from './note' describe('WidgetNote', () => { let mockResizeObserver: { observe: ReturnType disconnect: ReturnType unobserve: ReturnType } beforeEach(() => { // Mock ResizeObserver mockResizeObserver = { observe: vi.fn(), disconnect: vi.fn(), unobserve: vi.fn(), } global.ResizeObserver = class { observe = mockResizeObserver.observe disconnect = mockResizeObserver.disconnect unobserve = mockResizeObserver.unobserve } as unknown as typeof ResizeObserver // Default: no overflow vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) }) afterEach(() => { vi.restoreAllMocks() }) test('returns null when children is undefined', () => { // @ts-expect-error - testing runtime behavior with undefined const { container } = render({undefined}) expect(container.firstChild).toBeNull() }) test('returns null when children is empty string', () => { const { container } = render({''}) expect(container.firstChild).toBeNull() }) test('returns null when children is null', () => { // @ts-expect-error - testing runtime behavior with null const { container } = render({null}) expect(container.firstChild).toBeNull() }) test('renders Markdown component when children exists', () => { render(Simple note text) expect(screen.getByText('Simple note text')).toBeTruthy() }) test('renders markdown-formatted note content', () => { render({'# Heading\n\nParagraph text'}) expect(screen.getByText('Heading')).toBeTruthy() expect(screen.getByText('Paragraph text')).toBeTruthy() }) test('renders note with list items', () => { render({'- Item 1\n- Item 2\n- Item 3'}) expect(screen.getByText('Item 1')).toBeTruthy() expect(screen.getByText('Item 2')).toBeTruthy() expect(screen.getByText('Item 3')).toBeTruthy() }) test('renders note with links', () => { render( {'Check out [this link](https://example.com)'}, ) const link = screen.getByText('this link') expect(link.tagName).toBe('A') expect(link.getAttribute('href')).toBe('https://example.com') }) test('renders note with complex markdown', () => { render( {`# Title ## Subtitle Paragraph with **bold** and *italic*. - List item 1 - List item 2 [Link](https://example.com)`}, ) expect(screen.getByText('Title')).toBeTruthy() expect(screen.getByText('Subtitle')).toBeTruthy() expect(screen.getByText('bold')).toBeTruthy() expect(screen.getByText('italic')).toBeTruthy() expect(screen.getByText('List item 1')).toBeTruthy() expect(screen.getByText('List item 2')).toBeTruthy() expect(screen.getByText('Link')).toBeTruthy() }) test('renders note with bold text', () => { render({'This is **bold** text'}) expect(screen.getByText('bold')).toBeTruthy() }) test('renders note with italic text', () => { render({'This is *italic* text'}) expect(screen.getByText('italic')).toBeTruthy() }) test('renders note with code blocks', () => { render({'Here is `inline code`'}) expect(screen.getByText('inline code')).toBeTruthy() }) test('renders note with blockquote', () => { render({'> This is a quote'}) expect(screen.getByText('This is a quote')).toBeTruthy() }) test('uses default labels', () => { // Mock overflow to show toggle button vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n'.repeat(10)}) expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy() }) test('uses custom labels', async () => { const user = userEvent.setup() // Mock overflow to show toggle button vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render( {'Long content\n'.repeat(10)} , ) const button = screen.getByRole('button', { name: 'Expand' }) expect(button).toBeTruthy() await user.click(button) await waitFor(() => { expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy() }) }) test('merges custom labels with defaults', async () => { const user = userEvent.setup() // Mock overflow to show toggle button vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render( {'Long content\n'.repeat(10)} , ) const button = await screen.findByRole('button', { name: 'Expand' }) expect(button).toBeTruthy() await user.click(button) await waitFor(() => { // showLess should use default since not provided expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy() }) }) describe('Expand/Collapse functionality', () => { test('does not show toggle button when content does not overflow', () => { // Content fits within container (no overflow) vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render(Short content) expect(screen.queryByRole('button')).toBeNull() }) test('shows toggle button when content overflows', async () => { // Content overflows container vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) await waitFor(() => { expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy() }) }) test('toggles expansion state on button click', async () => { const user = userEvent.setup() // Content overflows container vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button', { name: 'Show More' }) expect(button.getAttribute('aria-expanded')).toBe('false') await user.click(button) await waitFor(() => { expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy() expect( screen .getByRole('button', { name: 'Show Less' }) .getAttribute('aria-expanded'), ).toBe('true') }) }) test('collapses content on second button click', async () => { const user = userEvent.setup() vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button', { name: 'Show More' }) // Expand await user.click(button) await waitFor(() => { expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy() }) // Collapse await user.click(screen.getByRole('button', { name: 'Show Less' })) await waitFor(() => { expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy() expect( screen .getByRole('button', { name: 'Show More' }) .getAttribute('aria-expanded'), ).toBe('false') }) }) test('sets up ResizeObserver on mount', () => { render(Test content) // Verify ResizeObserver was used by checking observe was called expect(mockResizeObserver.observe).toHaveBeenCalled() }) test('disconnects ResizeObserver on unmount', () => { const { unmount } = render(Test content) unmount() expect(mockResizeObserver.disconnect).toHaveBeenCalled() }) test('recalculates overflow when ResizeObserver triggers', async () => { let resizeCallback: ResizeObserverCallback global.ResizeObserver = class { constructor(callback: ResizeObserverCallback) { resizeCallback = callback } observe = mockResizeObserver.observe disconnect = mockResizeObserver.disconnect unobserve = mockResizeObserver.unobserve } as unknown as typeof ResizeObserver // Start with no overflow vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render(Test content) expect(screen.queryByRole('button')).toBeNull() // Simulate overflow after resize vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) // Trigger resize callback resizeCallback!([], mockResizeObserver as unknown as ResizeObserver) await waitFor(() => { expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy() }) }) test('maintains expanded state correctly after multiple toggles', async () => { const user = userEvent.setup() vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button') // Toggle multiple times await user.click(button) // Expand await user.click(button) // Collapse await user.click(button) // Expand await user.click(button) // Collapse await waitFor(() => { expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy() expect(button.getAttribute('aria-expanded')).toBe('false') }) }) }) describe('Accessibility', () => { test('button has aria-expanded attribute', async () => { vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button') expect(button.getAttribute('aria-expanded')).toBe('false') }) test('button has aria-controls attribute', async () => { vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button') expect(button.getAttribute('aria-controls')).toBe('note-content') }) test('button has aria-label for Show More', async () => { vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button', { name: 'Show More' }) expect(button.getAttribute('aria-label')).toBe('Show More') }) test('button has aria-label for Show Less when expanded', async () => { const user = userEvent.setup() vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue( 100, ) vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60) render({'Long content\n\n'.repeat(10)}) const button = await screen.findByRole('button') await user.click(button) await waitFor(() => { const expandedButton = screen.getByRole('button', { name: 'Show Less' }) expect(expandedButton.getAttribute('aria-label')).toBe('Show Less') }) }) }) })