mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
* Changes from fix/agent-output-summary-for-pipeline-steps * feat: Optimize pipeline summary extraction and fix regex vulnerability * fix: Use fallback summary for pipeline steps when extraction fails * fix: Strip follow-up session scaffold from pipeline step fallback summaries
239 lines
8.8 KiB
TypeScript
239 lines
8.8 KiB
TypeScript
/**
|
|
* Unit tests for the summary auto-scroll detection logic.
|
|
*
|
|
* These tests verify the behavior of the scroll detection function used in
|
|
* AgentOutputModal to determine if auto-scroll should be enabled.
|
|
*
|
|
* The logic mirrors the handleSummaryScroll function in:
|
|
* apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
|
|
*
|
|
* Auto-scroll behavior:
|
|
* - When user is at or near the bottom (< 50px from bottom), auto-scroll is enabled
|
|
* - When user scrolls up to view older content, auto-scroll is disabled
|
|
* - Scrolling back to bottom re-enables auto-scroll
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
/**
|
|
* Determines if the scroll position is at the bottom of the container.
|
|
* This is the core logic from handleSummaryScroll in AgentOutputModal.
|
|
*
|
|
* @param scrollTop - Current scroll position from top
|
|
* @param scrollHeight - Total scrollable height
|
|
* @param clientHeight - Visible height of the container
|
|
* @param threshold - Distance from bottom to consider "at bottom" (default: 50px)
|
|
* @returns true if at bottom, false otherwise
|
|
*/
|
|
function isScrollAtBottom(
|
|
scrollTop: number,
|
|
scrollHeight: number,
|
|
clientHeight: number,
|
|
threshold = 50
|
|
): boolean {
|
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
return distanceFromBottom < threshold;
|
|
}
|
|
|
|
describe('Summary Auto-Scroll Detection Logic', () => {
|
|
describe('basic scroll position detection', () => {
|
|
it('should return true when scrolled to exact bottom', () => {
|
|
// Container: 500px tall, content: 1000px tall
|
|
// ScrollTop: 500 (scrolled to bottom)
|
|
const result = isScrollAtBottom(500, 1000, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return true when near bottom (within threshold)', () => {
|
|
// 49px from bottom - within 50px threshold
|
|
const result = isScrollAtBottom(451, 1000, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return true when exactly at threshold boundary (49px)', () => {
|
|
// 49px from bottom
|
|
const result = isScrollAtBottom(451, 1000, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when just outside threshold (51px)', () => {
|
|
// 51px from bottom - outside 50px threshold
|
|
const result = isScrollAtBottom(449, 1000, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when scrolled to top', () => {
|
|
const result = isScrollAtBottom(0, 1000, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when scrolled to middle', () => {
|
|
const result = isScrollAtBottom(250, 1000, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('edge cases with small content', () => {
|
|
it('should return true when content fits in viewport (no scroll needed)', () => {
|
|
// Content is smaller than container - no scrolling possible
|
|
const result = isScrollAtBottom(0, 300, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return true when content exactly fits viewport', () => {
|
|
const result = isScrollAtBottom(0, 500, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return true when content slightly exceeds viewport (within threshold)', () => {
|
|
// Content: 540px, Viewport: 500px, can scroll 40px
|
|
// At scroll 0, we're 40px from bottom - within threshold
|
|
const result = isScrollAtBottom(0, 540, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('large content scenarios', () => {
|
|
it('should correctly detect bottom in very long content', () => {
|
|
// Simulate accumulated summary from many pipeline steps
|
|
// Content: 10000px, Viewport: 500px
|
|
const result = isScrollAtBottom(9500, 10000, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should correctly detect non-bottom in very long content', () => {
|
|
// User scrolled up to read earlier summaries
|
|
const result = isScrollAtBottom(5000, 10000, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should detect when user scrolls up from bottom', () => {
|
|
// Started at bottom (scroll: 9500), then scrolled up 100px
|
|
const result = isScrollAtBottom(9400, 10000, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('custom threshold values', () => {
|
|
it('should work with larger threshold (100px)', () => {
|
|
// 75px from bottom - within 100px threshold
|
|
const result = isScrollAtBottom(425, 1000, 500, 100);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should work with smaller threshold (10px)', () => {
|
|
// 15px from bottom - outside 10px threshold
|
|
const result = isScrollAtBottom(485, 1000, 500, 10);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should work with zero threshold (exact match only)', () => {
|
|
// At exact bottom - distanceFromBottom = 0, which is NOT < 0 with strict comparison
|
|
// This is an edge case: the implementation uses < not <=
|
|
const result = isScrollAtBottom(500, 1000, 500, 0);
|
|
expect(result).toBe(false); // 0 < 0 is false
|
|
|
|
// 1px from bottom - also fails
|
|
const result2 = isScrollAtBottom(499, 1000, 500, 0);
|
|
expect(result2).toBe(false);
|
|
|
|
// For exact match with 0 threshold, we need negative distanceFromBottom
|
|
// which happens when scrollTop > scrollHeight - clientHeight (overscroll)
|
|
const result3 = isScrollAtBottom(501, 1000, 500, 0);
|
|
expect(result3).toBe(true); // -1 < 0 is true
|
|
});
|
|
});
|
|
|
|
describe('pipeline summary scrolling scenarios', () => {
|
|
it('should enable auto-scroll when new content arrives while at bottom', () => {
|
|
// User is at bottom viewing step 2 summary
|
|
// Step 3 summary is added, increasing scrollHeight from 1000 to 1500
|
|
// ScrollTop stays at 950 (was at bottom), but now user needs to scroll
|
|
|
|
// Before new content: isScrollAtBottom(950, 1000, 500) = true
|
|
// After new content: auto-scroll should kick in to scroll to new bottom
|
|
|
|
// Simulating the auto-scroll effect setting scrollTop to new bottom
|
|
const newScrollTop = 1500 - 500; // scrollHeight - clientHeight
|
|
const result = isScrollAtBottom(newScrollTop, 1500, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should not auto-scroll when user is reading earlier summaries', () => {
|
|
// User scrolled up to read step 1 summary while step 3 is added
|
|
// scrollHeight increases, but scrollTop stays same
|
|
// User is now further from bottom
|
|
|
|
// User was at scroll position 200 (reading early content)
|
|
// New content increases scrollHeight from 1000 to 1500
|
|
// Distance from bottom goes from 300 to 800
|
|
const result = isScrollAtBottom(200, 1500, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should re-enable auto-scroll when user scrolls back to bottom', () => {
|
|
// User was reading step 1 (scrollTop: 200)
|
|
// User scrolls back to bottom to see latest content
|
|
const result = isScrollAtBottom(1450, 1500, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('decimal scroll values', () => {
|
|
it('should handle fractional scroll positions', () => {
|
|
// Browsers can report fractional scroll values
|
|
const result = isScrollAtBottom(499.5, 1000, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle fractional scroll heights', () => {
|
|
const result = isScrollAtBottom(450.7, 1000.3, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('negative and invalid inputs', () => {
|
|
it('should handle negative scrollTop (bounce scroll)', () => {
|
|
// iOS can report negative scrollTop during bounce
|
|
const result = isScrollAtBottom(-10, 1000, 500);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle zero scrollHeight', () => {
|
|
// Empty content
|
|
const result = isScrollAtBottom(0, 0, 500);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle zero clientHeight', () => {
|
|
// Hidden container - distanceFromBottom = 1000 - 0 - 0 = 1000
|
|
// This is not < threshold, so returns false
|
|
// This edge case represents a broken/invisible container
|
|
const result = isScrollAtBottom(0, 1000, 0);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('real-world accumulated summary dimensions', () => {
|
|
it('should handle typical 3-step pipeline summary dimensions', () => {
|
|
// Approximate: 3 steps x ~800px each = ~2400px
|
|
// Viewport: 400px (modal height)
|
|
const result = isScrollAtBottom(2000, 2400, 400);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle large 10-step pipeline summary dimensions', () => {
|
|
// Approximate: 10 steps x ~800px each = ~8000px
|
|
// Viewport: 400px
|
|
const result = isScrollAtBottom(7600, 8000, 400);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should detect scroll to top of large summary', () => {
|
|
// User at top of 10-step summary
|
|
const result = isScrollAtBottom(0, 8000, 400);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
});
|