/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { cleanup, render, screen } from '@testing-library/react' import { afterEach, describe, expect, test, vi } from 'vitest' import { Response } from '../response' import { FADE_DURATION_MS, FADE_HYDRATION_THRESHOLD, FADE_STAGGER_MAX_MS, } from '../response-fade' afterEach(() => { cleanup() vi.restoreAllMocks() }) describe('Response streaming fade', () => { test('wraps newly streamed words when final is false', () => { const { rerender } = render(Hello) expect(document.querySelectorAll('[data-stream-fade]').length).toBeGreaterThan( 0 ) expect(screen.getByText('Hello')).toBeTruthy() rerender(Hello world) const fades = [...document.querySelectorAll('[data-stream-fade]')] expect(fades.some((node) => node.textContent === 'world')).toBe(true) }) test('renders settled content with zero fade wrappers when final is true', () => { render(Hello world) expect(document.querySelectorAll('[data-stream-fade]')).toHaveLength(0) expect(screen.getByText(/Hello world/)).toBeTruthy() }) test('does not fade inline code or fenced code blocks', () => { render( {['Use `code` and:', '', '```', 'block', '```'].join('\n')} ) const fades = [...document.querySelectorAll('[data-stream-fade]')] const fadedText = fades.map((node) => node.textContent ?? '').join('') expect(fadedText.includes('block')).toBe(false) expect( fades.every((node) => { const text = node.textContent ?? '' return text.trim() !== 'code' && text.trim() !== 'block' }) ).toBe(true) }) test('does not re-animate words after markdown restructuring around strong', () => { vi.spyOn(performance, 'now').mockReturnValue(1000) const { rerender } = render(**fin) expect(document.querySelectorAll('[data-stream-fade]').length).toBeGreaterThan( 0 ) vi.spyOn(performance, 'now').mockReturnValue( 1000 + FADE_DURATION_MS + FADE_STAGGER_MAX_MS + 1 ) rerender(**final**) const strong = document.querySelector('strong') expect(strong?.textContent).toContain('final') const fades = [...document.querySelectorAll('[data-stream-fade]')] expect( fades.every((node) => !(node.textContent ?? '').includes('final')) ).toBe(true) }) test('suppresses fades on the first streaming render of hydrated content', () => { const hydrated = 'word '.repeat(FADE_HYDRATION_THRESHOLD) render({hydrated}) expect(document.querySelectorAll('[data-stream-fade]')).toHaveLength(0) }) test('drops all fade wrappers once the stream settles', () => { const { rerender } = render( Streaming text ) expect(document.querySelectorAll('[data-stream-fade]').length).toBeGreaterThan( 0 ) rerender(Streaming text) expect(document.querySelectorAll('[data-stream-fade]')).toHaveLength(0) }) })