• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prabhuignoto / react-chrono / #92

18 Jun 2025 10:08AM UTC coverage: 90.727% (+0.9%) from 89.791%
#92

push

web-flow
Minor cleanup and expanding test coverage (#548)

* refactor: rename project to React Chrono UI and update related files

* fix: update tsconfig to reference the correct entry file for React Chrono UI

* feat: enhance styles with vendor prefixes and improve cross-browser support

* Add tests for useNewScrollPosition hook and TimelineHorizontal component

- Implement comprehensive tests for the useNewScrollPosition hook covering horizontal, vertical, and edge cases.
- Create a new test file for the TimelineHorizontal component, ensuring it renders correctly and handles various props and states.
- Update snapshots for timeline control and vertical components to reflect recent changes in class names.
- Modify vitest configuration to include all test files in the src directory.

* refactor: simplify transform handling in timeline styles

* refactor: clean up test imports and remove unused styles in timeline components

1783 of 2112 branches covered (84.42%)

Branch coverage included in aggregate %.

670 of 674 new or added lines in 12 files covered. (99.41%)

400 existing lines in 29 files now uncovered.

10564 of 11497 relevant lines covered (91.88%)

10.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.72
/src/utils/timelineUtils.ts
1
import React from 'react';
1✔
2

3
/**
4
 * Timeline utility functions for managing timeline functionality
5
 */
6

7
/**
8
 * Safely extracts searchable text from potentially complex React node content
9
 * Recursively processes React nodes into plain text for search operations
10
 * @param content - React node (string, array, or component) to extract text from
11
 * @returns Plain text string suitable for searching
12
 */
13
export const getSearchableText = (content: React.ReactNode): string => {
1✔
14
  if (content === null || content === undefined) {
241✔
15
    return '';
2✔
16
  }
2✔
17
  if (typeof content === 'string') {
241✔
18
    return content;
230✔
19
  }
230✔
20
  if (Array.isArray(content)) {
24✔
21
    return content
5✔
22
      .map((item) => getSearchableText(item))
5✔
23
      .filter(Boolean)
5✔
24
      .join(' ');
5✔
25
  }
5✔
26
  // Handle React elements with children or other complex node types
27
  if (React.isValidElement(content)) {
4✔
28
    const elementProps = content.props as { children?: React.ReactNode };
4✔
29
    if (elementProps.children) {
4✔
30
      return getSearchableText(elementProps.children);
4✔
31
    }
4✔
32
  }
4!
33
  return '';
×
34
};
×
35

36
/**
37
 * Safely validates if a URL belongs to a trusted video platform
38
 * @param url - URL to validate
39
 * @returns Object with validation result and platform info
40
 */
41
const validateVideoUrl = (
1✔
42
  url: string,
3✔
43
): { isValid: boolean; platform?: string; origin?: string } => {
3✔
44
  try {
3✔
45
    const parsedUrl = new URL(url);
3✔
46
    const hostname = parsedUrl.hostname.toLowerCase();
3✔
47

48
    // YouTube domains
49
    if (
3✔
50
      hostname === 'www.youtube.com' ||
3✔
51
      hostname === 'youtube.com' ||
1✔
52
      hostname === 'youtu.be'
1✔
53
    ) {
3✔
54
      return {
1✔
55
        isValid: true,
1✔
56
        platform: 'youtube',
1✔
57
        origin: 'https://www.youtube.com',
1✔
58
      };
1✔
59
    }
1✔
60

61
    // Vimeo domains
62
    if (hostname === 'vimeo.com' || hostname === 'player.vimeo.com') {
3!
63
      return { isValid: true, platform: 'vimeo', origin: 'https://vimeo.com' };
1✔
64
    }
1!
65

66
    // For other HTTPS origins, return the actual origin
UNCOV
67
    if (parsedUrl.protocol === 'https:') {
×
UNCOV
68
      return { isValid: true, platform: 'other', origin: parsedUrl.origin };
×
UNCOV
69
    }
×
70

UNCOV
71
    return { isValid: false };
×
72
  } catch {
3✔
73
    return { isValid: false };
1✔
74
  }
1✔
75
};
3✔
76

77
/**
78
 * Pauses video embeds (primarily YouTube) within an element
79
 * @param element - HTML element containing video iframes to pause
80
 */
81
export const pauseVideoEmbeds = (element: HTMLElement | null): void => {
1✔
82
  if (!element) return;
4✔
83

84
  try {
3✔
85
    const iframes = element.querySelectorAll('iframe');
3✔
86
    iframes.forEach((iframe) => {
3✔
87
      const src = iframe.getAttribute('src') || '';
3!
88
      if (!src || !iframe.contentWindow) return;
3!
89

90
      const validation = validateVideoUrl(src);
3✔
91
      if (!validation.isValid) return;
3✔
92

93
      const targetOrigin = validation.origin || '*';
3!
94

95
      try {
3✔
96
        // Send appropriate pause command based on platform
97
        let message = '';
3✔
98
        if (validation.platform === 'youtube') {
3✔
99
          message = '{"event":"command","func":"stopVideo","args":""}';
1✔
100
        } else if (validation.platform === 'vimeo') {
1✔
101
          message = '{"method":"pause"}';
1✔
102
        } else {
1!
103
          // Generic pause attempt for other platforms
UNCOV
104
          message = '{"event":"command","func":"pause","args":""}';
×
UNCOV
105
        }
✔
106

107
        iframe.contentWindow.postMessage(message, targetOrigin);
2✔
108
      } catch (error) {
3!
UNCOV
109
        console.error('Error sending message to iframe:', error);
×
UNCOV
110
      }
×
111
    });
3✔
112
  } catch (error) {
4!
UNCOV
113
    console.error('Error pausing video embeds:', error);
×
UNCOV
114
  }
×
115
};
4✔
116

117
/**
118
 * Toggles visibility of media elements (images and videos) within an element
119
 * @param element - HTML element containing media elements
120
 * @param isVisible - Whether media elements should be visible
121
 */
122
export const toggleMediaVisibility = (
1✔
123
  element: HTMLElement | null,
3✔
124
  isVisible: boolean,
3✔
125
): void => {
3✔
126
  if (!element) return;
3✔
127

128
  try {
2✔
129
    const mediaElements = element.querySelectorAll('img,video');
2✔
130
    mediaElements.forEach((ele) => {
2✔
131
      if (ele instanceof HTMLElement) {
4✔
132
        ele.style.visibility = isVisible ? 'visible' : 'hidden';
4✔
133
      }
4✔
134
    });
2✔
135
  } catch (error) {
3!
UNCOV
136
    console.error('Error toggling media visibility:', error);
×
UNCOV
137
  }
×
138
};
3✔
139

140
/**
141
 * Finds a timeline item element by its ID in either the main timeline or portal
142
 * @param itemId - ID of the timeline item to find
143
 * @param timelineMode - Current timeline mode (HORIZONTAL, VERTICAL, etc.)
144
 * @param portalId - ID of the portal container for horizontal timeline modes
145
 * @returns HTML element of the timeline item or null if not found
146
 */
147
export const findTimelineElement = (
1✔
148
  itemId: string,
23✔
149
  timelineMode: string,
23✔
150
  portalId: string,
23✔
151
): HTMLElement | null => {
23✔
152
  if (!itemId || !timelineMode) return null;
23✔
153

154
  try {
21✔
155
    const elementId = `timeline-${timelineMode.toLowerCase()}-item-${itemId}`;
21✔
156
    let targetElement = document.getElementById(elementId);
21✔
157

158
    // Check in portal for horizontal modes
159
    if (
21✔
160
      !targetElement &&
21✔
161
      portalId &&
20✔
162
      (timelineMode === 'HORIZONTAL' || timelineMode === 'HORIZONTAL_ALL')
19✔
163
    ) {
23✔
164
      const portalContainer = document.getElementById(portalId);
5✔
165
      if (portalContainer) {
5✔
166
        targetElement = portalContainer.querySelector(
1✔
167
          `#timeline-card-${itemId}`,
1✔
168
        );
1✔
169
      }
1✔
170
    }
5✔
171

172
    return targetElement;
21✔
173
  } catch (error) {
23!
UNCOV
174
    console.error('Error finding timeline element:', error);
×
UNCOV
175
    return null;
×
UNCOV
176
  }
×
177
};
23✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc