Skip to content

Commit e93a718

Browse files
fix: Address ScrollTimeline and ViewTimeline issues
- Add input validation to both ScrollTimeline and ViewTimeline constructors - Add public disconnect() method to ViewTimeline to fix memory leak - Remove browser tests and replace with comprehensive examples - Add validation error tests for both timeline implementations - Create scroll-timeline example with real vs mock behavior comparison 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Ivan Galiatin <trurl-master@users.noreply.github.com>
1 parent 1a9fa72 commit e93a718

File tree

8 files changed

+438
-148
lines changed

8 files changed

+438
-148
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import './scroll-timeline.module.css';
3+
4+
/**
5+
* ScrollTimeline Example Component
6+
*
7+
* This component demonstrates ScrollTimeline API usage and compares:
8+
* 1. Real browser ScrollTimeline behavior (if supported)
9+
* 2. Our jsdom-testing-mocks ScrollTimeline implementation
10+
*
11+
* The example shows how scroll-driven animations can be created and tested.
12+
*/
13+
export function ScrollTimelineExample() {
14+
const containerRef = useRef<HTMLDivElement>(null);
15+
const progressBarRef = useRef<HTMLDivElement>(null);
16+
const animatedElementRef = useRef<HTMLDivElement>(null);
17+
const [scrollProgress, setScrollProgress] = useState(0);
18+
const [isRealBrowser, setIsRealBrowser] = useState(false);
19+
const [mockCurrentTime, setMockCurrentTime] = useState<number | null>(null);
20+
21+
useEffect(() => {
22+
// Check if we're in a real browser with native ScrollTimeline support
23+
setIsRealBrowser(typeof window !== 'undefined' && 'ScrollTimeline' in window);
24+
25+
if (!containerRef.current || !progressBarRef.current || !animatedElementRef.current) {
26+
return;
27+
}
28+
29+
const container = containerRef.current;
30+
const progressBar = progressBarRef.current;
31+
const animatedElement = animatedElementRef.current;
32+
33+
try {
34+
// Create ScrollTimeline (works with both real browser and our mock)
35+
const scrollTimeline = new ScrollTimeline({
36+
source: container,
37+
axis: 'block'
38+
});
39+
40+
// Create animation using ScrollTimeline
41+
const animation = new Animation(
42+
new KeyframeEffect(
43+
animatedElement,
44+
[
45+
{ transform: 'translateX(0px)', backgroundColor: 'red' },
46+
{ transform: 'translateX(200px)', backgroundColor: 'blue' }
47+
],
48+
{ duration: 1000 }
49+
),
50+
scrollTimeline
51+
);
52+
53+
animation.play();
54+
55+
// Monitor scroll progress
56+
const updateProgress = () => {
57+
const scrollTop = container.scrollTop;
58+
const scrollHeight = container.scrollHeight - container.clientHeight;
59+
const progress = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
60+
61+
setScrollProgress(progress);
62+
setMockCurrentTime(scrollTimeline.currentTime);
63+
64+
// Update progress bar
65+
progressBar.style.width = `${progress}%`;
66+
};
67+
68+
container.addEventListener('scroll', updateProgress);
69+
updateProgress(); // Initial call
70+
71+
return () => {
72+
container.removeEventListener('scroll', updateProgress);
73+
animation.cancel();
74+
};
75+
} catch (error) {
76+
console.error('ScrollTimeline not available:', error);
77+
}
78+
}, []);
79+
80+
return (
81+
<div className="scroll-timeline-example">
82+
<h2>ScrollTimeline API Example</h2>
83+
84+
<div className="info-panel">
85+
<p><strong>Environment:</strong> {isRealBrowser ? 'Real Browser' : 'jsdom with Mock'}</p>
86+
<p><strong>Scroll Progress:</strong> {scrollProgress.toFixed(1)}%</p>
87+
<p><strong>Timeline.currentTime:</strong> {mockCurrentTime?.toFixed(1) || 'null'}</p>
88+
</div>
89+
90+
<div className="demo-container">
91+
<div
92+
ref={containerRef}
93+
className="scrollable-container"
94+
>
95+
<div className="scroll-content">
96+
<h3>Scroll down to see the animation</h3>
97+
<p>This content is much taller than the container to enable scrolling.</p>
98+
<p>The red square will move and change color as you scroll...</p>
99+
100+
{/* Fill content to make scrolling possible */}
101+
{Array.from({ length: 20 }, (_, i) => (
102+
<div key={i} className="content-section">
103+
<p>Content section {i + 1}</p>
104+
<p>Keep scrolling to see the ScrollTimeline animation in action.</p>
105+
</div>
106+
))}
107+
108+
<p>🎉 You've reached the end! The animation should be complete.</p>
109+
</div>
110+
</div>
111+
112+
<div className="animation-display">
113+
<div className="progress-container">
114+
<div className="progress-label">Scroll Progress:</div>
115+
<div className="progress-track">
116+
<div ref={progressBarRef} className="progress-bar"></div>
117+
</div>
118+
</div>
119+
120+
<div className="animated-element-container">
121+
<div ref={animatedElementRef} className="animated-element">
122+
📦
123+
</div>
124+
</div>
125+
</div>
126+
</div>
127+
128+
<div className="explanation">
129+
<h3>How it works:</h3>
130+
<ul>
131+
<li><strong>ScrollTimeline:</strong> Creates a timeline driven by scroll position</li>
132+
<li><strong>Animation:</strong> Uses the ScrollTimeline instead of time-based duration</li>
133+
<li><strong>Progress:</strong> Animation progress matches scroll progress (0-100%)</li>
134+
<li><strong>Mock vs Real:</strong> Our mock provides the same API as the real browser implementation</li>
135+
</ul>
136+
137+
<h3>Testing Benefits:</h3>
138+
<ul>
139+
<li>Test scroll-driven animations in jsdom without browser dependency</li>
140+
<li>Predictable behavior for automated testing</li>
141+
<li>Same API surface as real browser ScrollTimeline</li>
142+
<li>Easy to control scroll state programmatically in tests</li>
143+
</ul>
144+
</div>
145+
</div>
146+
);
147+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
.scroll-timeline-example {
2+
padding: 20px;
3+
max-width: 1000px;
4+
margin: 0 auto;
5+
}
6+
7+
.info-panel {
8+
background: #f0f8ff;
9+
border: 1px solid #007acc;
10+
border-radius: 8px;
11+
padding: 15px;
12+
margin-bottom: 20px;
13+
}
14+
15+
.info-panel p {
16+
margin: 5px 0;
17+
font-family: monospace;
18+
}
19+
20+
.demo-container {
21+
display: flex;
22+
gap: 20px;
23+
margin-bottom: 30px;
24+
}
25+
26+
.scrollable-container {
27+
width: 400px;
28+
height: 300px;
29+
border: 2px solid #333;
30+
border-radius: 8px;
31+
overflow-y: auto;
32+
background: #fafafa;
33+
}
34+
35+
.scroll-content {
36+
padding: 20px;
37+
height: 1200px; /* Much taller than container to enable scrolling */
38+
}
39+
40+
.content-section {
41+
margin: 20px 0;
42+
padding: 15px;
43+
background: rgba(0, 122, 204, 0.1);
44+
border-radius: 4px;
45+
}
46+
47+
.animation-display {
48+
flex: 1;
49+
display: flex;
50+
flex-direction: column;
51+
gap: 20px;
52+
}
53+
54+
.progress-container {
55+
display: flex;
56+
flex-direction: column;
57+
gap: 10px;
58+
}
59+
60+
.progress-label {
61+
font-weight: bold;
62+
color: #333;
63+
}
64+
65+
.progress-track {
66+
width: 100%;
67+
height: 20px;
68+
background: #e0e0e0;
69+
border-radius: 10px;
70+
overflow: hidden;
71+
border: 1px solid #ccc;
72+
}
73+
74+
.progress-bar {
75+
height: 100%;
76+
background: linear-gradient(90deg, #007acc, #00aaff);
77+
width: 0%;
78+
transition: width 0.1s ease;
79+
}
80+
81+
.animated-element-container {
82+
height: 200px;
83+
position: relative;
84+
background: rgba(240, 248, 255, 0.5);
85+
border: 2px dashed #007acc;
86+
border-radius: 8px;
87+
display: flex;
88+
align-items: center;
89+
}
90+
91+
.animated-element {
92+
width: 50px;
93+
height: 50px;
94+
background: red;
95+
border-radius: 8px;
96+
display: flex;
97+
align-items: center;
98+
justify-content: center;
99+
font-size: 24px;
100+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
101+
will-change: transform, background-color;
102+
}
103+
104+
.explanation {
105+
background: #f9f9f9;
106+
border-left: 4px solid #007acc;
107+
padding: 20px;
108+
margin-top: 20px;
109+
}
110+
111+
.explanation h3 {
112+
color: #007acc;
113+
margin-top: 0;
114+
margin-bottom: 10px;
115+
}
116+
117+
.explanation ul {
118+
margin: 10px 0;
119+
padding-left: 20px;
120+
}
121+
122+
.explanation li {
123+
margin-bottom: 8px;
124+
line-height: 1.4;
125+
}
126+
127+
/* Responsive design */
128+
@media (max-width: 768px) {
129+
.demo-container {
130+
flex-direction: column;
131+
}
132+
133+
.scrollable-container {
134+
width: 100%;
135+
}
136+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { mockAnimationsApi } from '../../../../../src/mocks/web-animations-api';
4+
import { ScrollTimelineExample } from './ScrollTimeline';
5+
6+
// Mock the ScrollTimeline API before tests
7+
beforeAll(() => {
8+
mockAnimationsApi();
9+
});
10+
11+
describe('ScrollTimeline Example', () => {
12+
it('should render the example component', () => {
13+
render(<ScrollTimelineExample />);
14+
15+
expect(screen.getByText('ScrollTimeline API Example')).toBeInTheDocument();
16+
expect(screen.getByText(/Environment:/)).toBeInTheDocument();
17+
expect(screen.getByText(/Scroll Progress:/)).toBeInTheDocument();
18+
});
19+
20+
it('should show jsdom environment info', () => {
21+
render(<ScrollTimelineExample />);
22+
23+
expect(screen.getByText(/jsdom with Mock/)).toBeInTheDocument();
24+
});
25+
26+
it('should render scrollable content', () => {
27+
render(<ScrollTimelineExample />);
28+
29+
expect(screen.getByText('Scroll down to see the animation')).toBeInTheDocument();
30+
expect(screen.getByText(/Keep scrolling to see the ScrollTimeline animation/)).toBeInTheDocument();
31+
});
32+
33+
it('should render animated element', () => {
34+
render(<ScrollTimelineExample />);
35+
36+
const animatedElement = screen.getByText('📦');
37+
expect(animatedElement).toBeInTheDocument();
38+
expect(animatedElement).toHaveClass('animated-element');
39+
});
40+
41+
it('should render progress bar', () => {
42+
const { container } = render(<ScrollTimelineExample />);
43+
44+
const progressBar = container.querySelector('.progress-bar');
45+
expect(progressBar).toBeInTheDocument();
46+
});
47+
48+
it('should render explanation section', () => {
49+
render(<ScrollTimelineExample />);
50+
51+
expect(screen.getByText('How it works:')).toBeInTheDocument();
52+
expect(screen.getByText('Testing Benefits:')).toBeInTheDocument();
53+
expect(screen.getByText(/Test scroll-driven animations in jsdom/)).toBeInTheDocument();
54+
});
55+
});

0 commit comments

Comments
 (0)