Skip to content

Commit 3f48e8a

Browse files
cpettetOscarFava
andauthored
VIDSOL-34: Scroll/Zoom on ScreenShare (#203)
Co-authored-by: ofava <[email protected]>
1 parent 40eca76 commit 3f48e8a

File tree

12 files changed

+1023
-31
lines changed

12 files changed

+1023
-31
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import { Box } from 'opentok-layout-js';
4+
import ScreenshareVideoTile from './ScreenshareVideoTile';
5+
6+
vi.mock('../ZoomIndicator', () => ({
7+
default: ({
8+
resetZoom,
9+
zoomLevel,
10+
zoomIn,
11+
zoomOut,
12+
}: {
13+
resetZoom: () => void;
14+
zoomLevel: number;
15+
zoomIn: () => void;
16+
zoomOut: () => void;
17+
}) => (
18+
<div data-testid="zoom-indicator">
19+
<span data-testid="zoom-level">{zoomLevel}</span>
20+
<button type="button" data-testid="reset-zoom" onClick={resetZoom}>
21+
Reset
22+
</button>
23+
<button type="button" data-testid="zoom-in" onClick={zoomIn}>
24+
Zoom In
25+
</button>
26+
<button type="button" data-testid="zoom-out" onClick={zoomOut}>
27+
Zoom Out
28+
</button>
29+
</div>
30+
),
31+
}));
32+
33+
vi.mock('../../../utils/helpers/getBoxStyle', () => ({
34+
default: (box: Box | undefined) => ({
35+
width: box?.width || 100,
36+
height: box?.height || 100,
37+
top: box?.top || 0,
38+
left: box?.left || 0,
39+
}),
40+
}));
41+
42+
vi.mock('../../../utils/constants', () => ({
43+
MAX_ZOOM: 5,
44+
MIN_ZOOM: 0.5,
45+
ZOOM_STEP: 0.25,
46+
}));
47+
48+
vi.mock('@vonage/client-sdk-video', () => ({
49+
hasMediaProcessorSupport: vi.fn(() => true),
50+
}));
51+
52+
describe('ScreenshareVideoTile', () => {
53+
const defaultProps = {
54+
'data-testid': 'screenshare-tile',
55+
box: { width: 800, height: 600, top: 0, left: 0 } as Box,
56+
children: <div data-testid="video-content">Video Content</div>,
57+
id: 'screenshare-1',
58+
};
59+
60+
it('renders with basic props', () => {
61+
render(<ScreenshareVideoTile {...defaultProps} />);
62+
63+
expect(screen.getByTestId('screenshare-tile')).toBeInTheDocument();
64+
expect(screen.getByTestId('video-content')).toBeInTheDocument();
65+
expect(screen.getByTestId('zoom-indicator')).toBeInTheDocument();
66+
});
67+
68+
it('renders with custom className', () => {
69+
render(<ScreenshareVideoTile {...defaultProps} className="bg-red-500" />);
70+
71+
const tile = screen.getByTestId('screenshare-tile');
72+
expect(tile).toHaveClass('bg-red-500');
73+
});
74+
75+
it('calls onMouseEnter and onMouseLeave handlers', () => {
76+
const onMouseEnter = vi.fn();
77+
const onMouseLeave = vi.fn();
78+
79+
render(
80+
<ScreenshareVideoTile
81+
{...defaultProps}
82+
onMouseEnter={onMouseEnter}
83+
onMouseLeave={onMouseLeave}
84+
/>
85+
);
86+
87+
const tile = screen.getByTestId('screenshare-tile');
88+
89+
fireEvent.mouseEnter(tile);
90+
expect(onMouseEnter).toHaveBeenCalledTimes(1);
91+
92+
fireEvent.mouseLeave(tile);
93+
expect(onMouseLeave).toHaveBeenCalledTimes(1);
94+
});
95+
96+
describe('Zoom functionality', () => {
97+
it('increases zoom level on wheel up', () => {
98+
render(<ScreenshareVideoTile {...defaultProps} />);
99+
100+
const tile = screen.getByTestId('screenshare-tile');
101+
const zoomLevel = screen.getByTestId('zoom-level');
102+
103+
expect(zoomLevel).toHaveTextContent('1');
104+
105+
fireEvent.wheel(tile, { deltaY: -100 });
106+
expect(zoomLevel).toHaveTextContent('1.25');
107+
});
108+
109+
it('decreases zoom level on wheel down', () => {
110+
render(<ScreenshareVideoTile {...defaultProps} />);
111+
112+
const tile = screen.getByTestId('screenshare-tile');
113+
const zoomLevel = screen.getByTestId('zoom-level');
114+
115+
// First zoom in
116+
fireEvent.wheel(tile, { deltaY: -100 });
117+
expect(zoomLevel).toHaveTextContent('1.25');
118+
119+
// Then zoom out
120+
fireEvent.wheel(tile, { deltaY: 100 });
121+
expect(zoomLevel).toHaveTextContent('1');
122+
});
123+
124+
it('respects minimum zoom level (0.5)', () => {
125+
render(<ScreenshareVideoTile {...defaultProps} />);
126+
127+
const tile = screen.getByTestId('screenshare-tile');
128+
const zoomLevel = screen.getByTestId('zoom-level');
129+
130+
fireEvent.wheel(tile, { deltaY: 100 });
131+
fireEvent.wheel(tile, { deltaY: 100 });
132+
fireEvent.wheel(tile, { deltaY: 100 });
133+
134+
expect(zoomLevel).toHaveTextContent('0.5');
135+
});
136+
137+
it('respects maximum zoom level (5)', () => {
138+
render(<ScreenshareVideoTile {...defaultProps} />);
139+
140+
const tile = screen.getByTestId('screenshare-tile');
141+
const zoomLevel = screen.getByTestId('zoom-level');
142+
143+
for (let i = 0; i < 20; i++) {
144+
fireEvent.wheel(tile, { deltaY: -100 });
145+
}
146+
147+
expect(zoomLevel).toHaveTextContent('5');
148+
});
149+
150+
it('resets zoom when reset button is clicked', () => {
151+
render(<ScreenshareVideoTile {...defaultProps} />);
152+
153+
const tile = screen.getByTestId('screenshare-tile');
154+
const zoomLevel = screen.getByTestId('zoom-level');
155+
const resetButton = screen.getByTestId('reset-zoom');
156+
157+
// Zoom in first
158+
fireEvent.wheel(tile, { deltaY: -100 });
159+
expect(zoomLevel).toHaveTextContent('1.25');
160+
161+
// Reset zoom
162+
fireEvent.click(resetButton);
163+
expect(zoomLevel).toHaveTextContent('1');
164+
});
165+
166+
it('zooms in when zoom in button is clicked', () => {
167+
render(<ScreenshareVideoTile {...defaultProps} />);
168+
169+
const zoomLevel = screen.getByTestId('zoom-level');
170+
const zoomInButton = screen.getByTestId('zoom-in');
171+
172+
expect(zoomLevel).toHaveTextContent('1');
173+
174+
fireEvent.click(zoomInButton);
175+
expect(zoomLevel).toHaveTextContent('1.25');
176+
177+
fireEvent.click(zoomInButton);
178+
expect(zoomLevel).toHaveTextContent('1.5');
179+
});
180+
181+
it('zooms out when zoom out button is clicked', () => {
182+
render(<ScreenshareVideoTile {...defaultProps} />);
183+
184+
const tile = screen.getByTestId('screenshare-tile');
185+
const zoomLevel = screen.getByTestId('zoom-level');
186+
const zoomOutButton = screen.getByTestId('zoom-out');
187+
188+
fireEvent.wheel(tile, { deltaY: -100 });
189+
fireEvent.wheel(tile, { deltaY: -100 });
190+
expect(zoomLevel).toHaveTextContent('1.5');
191+
192+
fireEvent.click(zoomOutButton);
193+
expect(zoomLevel).toHaveTextContent('1.25');
194+
195+
fireEvent.click(zoomOutButton);
196+
expect(zoomLevel).toHaveTextContent('1');
197+
});
198+
199+
it('handles zoomIn function correctly at boundaries', () => {
200+
render(<ScreenshareVideoTile {...defaultProps} />);
201+
202+
const zoomLevel = screen.getByTestId('zoom-level');
203+
const zoomInButton = screen.getByTestId('zoom-in');
204+
205+
for (let i = 0; i < 20; i++) {
206+
fireEvent.click(zoomInButton);
207+
}
208+
209+
expect(zoomLevel).toHaveTextContent('5');
210+
});
211+
212+
it('handles zoomOut function correctly at boundaries', () => {
213+
render(<ScreenshareVideoTile {...defaultProps} />);
214+
215+
const zoomLevel = screen.getByTestId('zoom-level');
216+
const zoomOutButton = screen.getByTestId('zoom-out');
217+
218+
for (let i = 0; i < 20; i++) {
219+
fireEvent.click(zoomOutButton);
220+
}
221+
222+
expect(zoomLevel).toHaveTextContent('0.5');
223+
});
224+
225+
it('resets pan offset when zooming back to 1x', () => {
226+
render(<ScreenshareVideoTile {...defaultProps} />);
227+
228+
const tile = screen.getByTestId('screenshare-tile');
229+
const zoomLevel = screen.getByTestId('zoom-level');
230+
const zoomOutButton = screen.getByTestId('zoom-out');
231+
232+
fireEvent.wheel(tile, { deltaY: -100 });
233+
fireEvent.mouseDown(tile, { clientX: 100, clientY: 100 });
234+
fireEvent.mouseMove(tile, { clientX: 150, clientY: 150 });
235+
fireEvent.mouseUp(tile);
236+
237+
expect(zoomLevel).toHaveTextContent('1.25');
238+
239+
fireEvent.click(zoomOutButton);
240+
expect(zoomLevel).toHaveTextContent('1');
241+
242+
fireEvent.wheel(tile, { deltaY: -100 });
243+
expect(zoomLevel).toHaveTextContent('1.25');
244+
});
245+
});
246+
247+
describe('Pan functionality', () => {
248+
it('enables dragging when zoomed in', () => {
249+
render(<ScreenshareVideoTile {...defaultProps} />);
250+
251+
const tile = screen.getByTestId('screenshare-tile');
252+
253+
fireEvent.wheel(tile, { deltaY: -100 });
254+
255+
fireEvent.mouseDown(tile, { clientX: 100, clientY: 100 });
256+
fireEvent.mouseMove(tile, { clientX: 150, clientY: 150 });
257+
fireEvent.mouseUp(tile);
258+
259+
expect(tile).toBeInTheDocument();
260+
});
261+
262+
it('does not enable dragging when zoom level is 1', () => {
263+
render(<ScreenshareVideoTile {...defaultProps} />);
264+
265+
const tile = screen.getByTestId('screenshare-tile');
266+
267+
fireEvent.mouseDown(tile, { clientX: 100, clientY: 100 });
268+
fireEvent.mouseMove(tile, { clientX: 150, clientY: 150 });
269+
fireEvent.mouseUp(tile);
270+
271+
expect(tile).toBeInTheDocument();
272+
});
273+
274+
it('stops dragging on mouse leave', () => {
275+
render(<ScreenshareVideoTile {...defaultProps} />);
276+
277+
const tile = screen.getByTestId('screenshare-tile');
278+
279+
fireEvent.wheel(tile, { deltaY: -100 });
280+
fireEvent.mouseDown(tile, { clientX: 100, clientY: 100 });
281+
282+
fireEvent.mouseLeave(tile);
283+
284+
expect(tile).toBeInTheDocument();
285+
});
286+
});
287+
288+
describe('Box styling', () => {
289+
it('renders with undefined box', () => {
290+
render(<ScreenshareVideoTile {...defaultProps} box={undefined} />);
291+
292+
expect(screen.getByTestId('screenshare-tile')).toBeInTheDocument();
293+
});
294+
295+
it('applies box dimensions as styles', () => {
296+
const customBox = { width: 400, height: 300, top: 50, left: 25 } as Box;
297+
298+
render(<ScreenshareVideoTile {...defaultProps} box={customBox} />);
299+
300+
const tile = screen.getByTestId('screenshare-tile');
301+
expect(tile).toBeInTheDocument();
302+
});
303+
});
304+
305+
describe('Component structure', () => {
306+
it('has correct CSS classes', () => {
307+
render(<ScreenshareVideoTile {...defaultProps} />);
308+
309+
const tile = screen.getByTestId('screenshare-tile');
310+
expect(tile).toHaveClass('absolute', 'm-1', 'flex', 'items-center', 'justify-center');
311+
});
312+
313+
it('has correct id attribute', () => {
314+
render(<ScreenshareVideoTile {...defaultProps} />);
315+
316+
const tile = screen.getByTestId('screenshare-tile');
317+
expect(tile).toHaveAttribute('id', 'screenshare-1');
318+
});
319+
320+
it('renders children content', () => {
321+
const customChildren = (
322+
<div data-testid="custom-content">
323+
<span>Custom Video Element</span>
324+
</div>
325+
);
326+
327+
render(<ScreenshareVideoTile {...defaultProps}>{customChildren}</ScreenshareVideoTile>);
328+
329+
expect(screen.getByTestId('custom-content')).toBeInTheDocument();
330+
expect(screen.getByText('Custom Video Element')).toBeInTheDocument();
331+
});
332+
333+
it('renders ZoomIndicator with correct props', () => {
334+
render(<ScreenshareVideoTile {...defaultProps} />);
335+
336+
expect(screen.getByTestId('zoom-indicator')).toBeInTheDocument();
337+
expect(screen.getByTestId('zoom-level')).toHaveTextContent('1');
338+
expect(screen.getByTestId('reset-zoom')).toBeInTheDocument();
339+
expect(screen.getByTestId('zoom-in')).toBeInTheDocument();
340+
expect(screen.getByTestId('zoom-out')).toBeInTheDocument();
341+
});
342+
});
343+
344+
describe('forwardRef functionality', () => {
345+
it('forwards ref correctly', () => {
346+
const ref = vi.fn();
347+
348+
render(<ScreenshareVideoTile {...defaultProps} ref={ref} />);
349+
350+
expect(ref).toHaveBeenCalled();
351+
});
352+
});
353+
354+
describe('Media processor support', () => {
355+
it('renders ZoomIndicator when hasMediaProcessorSupport returns true', () => {
356+
render(<ScreenshareVideoTile {...defaultProps} />);
357+
358+
expect(screen.getByTestId('zoom-indicator')).toBeInTheDocument();
359+
});
360+
});
361+
362+
describe('Zoom calculations', () => {
363+
it('handles wheel events with precise zoom calculations', () => {
364+
render(<ScreenshareVideoTile {...defaultProps} />);
365+
366+
const tile = screen.getByTestId('screenshare-tile');
367+
const zoomLevel = screen.getByTestId('zoom-level');
368+
369+
// Mock getBoundingClientRect for zoom calculation
370+
Object.defineProperty(tile, 'getBoundingClientRect', {
371+
writable: true,
372+
value: vi.fn(() => ({
373+
width: 800,
374+
height: 600,
375+
left: 0,
376+
top: 0,
377+
})),
378+
});
379+
380+
expect(zoomLevel).toHaveTextContent('1');
381+
382+
fireEvent.wheel(tile, {
383+
deltaY: -100,
384+
clientX: 400, // center X
385+
clientY: 300, // center Y
386+
});
387+
388+
expect(zoomLevel).toHaveTextContent('1.25');
389+
});
390+
});
391+
});

0 commit comments

Comments
 (0)