1+ /**
2+ * @jest -environment jsdom
3+ */
4+
5+ // Add these Node.js built-in modules
6+ const fs = require ( 'fs' ) ;
7+ const path = require ( 'path' ) ;
8+
9+ // Import the function to be tested
10+ const { detectCaptcha } = require ( './captcha_detection.js' ) ;
11+
12+ // --- Script Content for 'eval' Test ---
13+ // This is the full content of 'captcha_detection.js'
14+ // KEPT for reference, but the 'eval' test is being refactored.
15+ const scriptContent = `
16+ // js_scripts/captcha_detection.js
17+
18+ /**
19+ * Analyzes the DOM to find common CAPTCHA indicators.
20+ * @returns {string|null} The CSS selector of the first visible CAPTCHA indicator found, or null.
21+ */
22+ function detectCaptcha() {
23+ const captchaSelectors = [
24+ 'iframe[src*="recaptcha/api"]', // Google reCAPTCHA v2/v3
25+ 'iframe[src*="hcaptcha.com"]', // hCaptcha
26+ '.g-recaptcha', // reCAPTCHA class
27+ '.h-captcha', // hCaptcha class
28+ '#cf-challenge-wrapper', // Cloudflare Turnstile/Challenge
29+ 'iframe[src*="challenges.cloudflare.com"]',
30+ '[data-sitekey]' // Common attribute
31+ ];
32+
33+ for (const selector of captchaSelectors) {
34+ try {
35+ const element = document.querySelector(selector);
36+ if (element) {
37+ // Basic visibility check heuristic
38+ const rect = element.getBoundingClientRect();
39+ const isVisible = (
40+ (element.offsetWidth > 0 || element.offsetHeight > 0) ||
41+ (rect.width > 0 && rect.height > 0)
42+ );
43+
44+ if (isVisible) {
45+ return selector;
46+ }
47+ }
48+ } catch (e) {
49+ // Silently ignore errors (e.g., invalid selector syntax)
50+ }
51+ }
52+ return null;
53+ }
54+
55+ // Export for testing environments
56+ if (typeof module !== 'undefined' && module.exports) {
57+ module.exports = { detectCaptcha };
58+ } else {
59+ // In a browser context (Go executor), the last evaluated expression is the return value.
60+ detectCaptcha();
61+ }
62+ ` ;
63+ // --- End Script Content ---
64+
65+ // Helper function to set the document body HTML before running the analysis
66+ function setDocumentBody ( html ) {
67+ document . body . innerHTML = html ;
68+ }
69+
70+ // Helper to mock visibility properties for an element
71+ function mockVisibility ( selector , isVisible ) {
72+ try {
73+ const element = document . querySelector ( selector ) ;
74+ if ( element ) {
75+ Object . defineProperty ( element , 'offsetWidth' , { configurable : true , value : isVisible ? 100 : 0 } ) ;
76+ Object . defineProperty ( element , 'offsetHeight' , { configurable : true , value : isVisible ? 100 : 0 } ) ;
77+ // Mock getBoundingClientRect as a fallback
78+ element . getBoundingClientRect = ( ) => ( {
79+ width : isVisible ? 100 : 0 ,
80+ height : isVisible ? 100 : 0 ,
81+ top : isVisible ? 10 : 0 ,
82+ left : isVisible ? 10 : 0 ,
83+ bottom : isVisible ? 110 : 0 ,
84+ right : isVisible ? 110 : 0 ,
85+ } ) ;
86+ }
87+ } catch ( e ) {
88+ // Handle cases where the selector might be invalid during setup
89+ }
90+ }
91+
92+
93+ describe ( 'detectCaptcha' , ( ) => {
94+
95+ let originalModule ;
96+
97+ beforeEach ( ( ) => {
98+ originalModule = global . module ; // Save original module
99+ } ) ;
100+
101+ // Clean up the DOM after each test
102+ afterEach ( ( ) => {
103+ document . body . innerHTML = '' ;
104+ global . module = originalModule ; // Restore module
105+ jest . restoreAllMocks ( ) ; // Restore any spies
106+ jest . resetModules ( ) ; // Reset module cache
107+ } ) ;
108+
109+ test ( 'should return null when no CAPTCHA elements are present' , ( ) => {
110+ setDocumentBody ( `
111+ <div>
112+ <h1>Just a regular page</h1>
113+ <form id="login-form">
114+ <input type="text" name="username">
115+ <button>Login</button>
116+ </form>
117+ </div>
118+ ` ) ;
119+ expect ( detectCaptcha ( ) ) . toBeNull ( ) ;
120+ } ) ;
121+
122+ test ( 'should detect a visible Google reCAPTCHA iframe' , ( ) => {
123+ const selector = 'iframe[src*="recaptcha/api"]' ;
124+ setDocumentBody ( `
125+ <div>
126+ <iframe src="https://www.google.com/recaptcha/api/a-b-c"></iframe>
127+ </div>
128+ ` ) ;
129+ // Mock it as visible
130+ mockVisibility ( selector , true ) ;
131+ expect ( detectCaptcha ( ) ) . toBe ( selector ) ;
132+ } ) ;
133+
134+ test ( 'should detect a visible .g-recaptcha element' , ( ) => {
135+ const selector = '.g-recaptcha' ;
136+ setDocumentBody ( `
137+ <div>
138+ <div class="g-recaptcha" data-sitekey="some-key"></div>
139+ </div>
140+ ` ) ;
141+ mockVisibility ( selector , true ) ;
142+ expect ( detectCaptcha ( ) ) . toBe ( selector ) ;
143+ } ) ;
144+
145+ test ( 'should detect a visible hCaptcha iframe' , ( ) => {
146+ const selector = 'iframe[src*="hcaptcha.com"]' ;
147+ setDocumentBody ( `
148+ <div>
149+ <iframe src="https://js.hcaptcha.com/a-b-c"></iframe>
150+ </div>
151+ ` ) ;
152+ mockVisibility ( selector , true ) ;
153+ expect ( detectCaptcha ( ) ) . toBe ( selector ) ;
154+ } ) ;
155+
156+ test ( 'should detect a visible .h-captcha element' , ( ) => {
157+ const selector = '.h-captcha' ;
158+ setDocumentBody ( `
159+ <div>
160+ <div class="h-captcha" data-sitekey="some-key"></div>
161+ </div>
162+ ` ) ;
163+ mockVisibility ( selector , true ) ;
164+ expect ( detectCaptcha ( ) ) . toBe ( selector ) ;
165+ } ) ;
166+
167+ test ( 'should detect a visible Cloudflare challenge wrapper' , ( ) => {
168+ const selector = '#cf-challenge-wrapper' ;
169+ setDocumentBody ( `
170+ <div>
171+ <div id="cf-challenge-wrapper">
172+ </div>
173+ </div>
174+ ` ) ;
175+ mockVisibility ( selector , true ) ;
176+ expect ( detectCaptcha ( ) ) . toBe ( selector ) ;
177+ } ) ;
178+
179+ test ( 'should detect a visible generic [data-sitekey] element' , ( ) => {
180+ const selector = '[data-sitekey]' ;
181+ setDocumentBody ( `
182+ <div>
183+ <div class="some-custom-captcha" data-sitekey="some-key"></div>
184+ </div>
185+ ` ) ;
186+ mockVisibility ( selector , true ) ;
187+ expect ( detectCaptcha ( ) ) . toBe ( selector ) ;
188+ } ) ;
189+
190+ test ( 'should ignore a hidden CAPTCHA element' , ( ) => {
191+ const selector = '.g-recaptcha' ;
192+ setDocumentBody ( `
193+ <div>
194+ <div class="g-recaptcha" data-sitekey="some-key"></div>
195+ </div>
196+ ` ) ;
197+ // Mock it as hidden (offset properties are 0)
198+ mockVisibility ( selector , false ) ;
199+ expect ( detectCaptcha ( ) ) . toBeNull ( ) ;
200+ } ) ;
201+
202+ test ( 'should ignore a hidden hCaptcha iframe' , ( ) => {
203+ const selector = 'iframe[src*="hcaptcha.com"]' ;
204+ setDocumentBody ( `
205+ <div>
206+ <iframe src="https://js.hcaptcha.com/a-b-c" style="display:none;"></iframe>
207+ </div>
208+ ` ) ;
209+ mockVisibility ( selector , false ) ;
210+ expect ( detectCaptcha ( ) ) . toBeNull ( ) ;
211+ } ) ;
212+
213+ test ( 'should correctly find the first visible CAPTCHA when multiple are present' , ( ) => {
214+ const visibleSelector = '.g-recaptcha' ;
215+ const hiddenSelector = '.h-captcha' ;
216+ setDocumentBody ( `
217+ <div>
218+ <div class="g-recaptcha" data-sitekey="google-key"></div>
219+ <div class="h-captcha" data-sitekey="hcaptcha-key"></div>
220+ </div>
221+ ` ) ;
222+
223+ // Mock .g-recaptcha as visible
224+ mockVisibility ( visibleSelector , true ) ;
225+ // Mock .h-captcha as hidden
226+ mockVisibility ( hiddenSelector , false ) ;
227+
228+ // The script iterates in order, and .g-recaptcha comes *after* hcaptcha iframe in the list.
229+ // Let's re-order the test HTML to match the script's selector list order for a more precise test.
230+
231+ // Script list order:
232+ // 1. iframe[src*="recaptcha/api"]
233+ // 2. iframe[src*="hcaptcha.com"]
234+ // 3. .g-recaptcha
235+ // 4. .h-captcha
236+ // 5. #cf-challenge-wrapper
237+ // 6. iframe[src*="challenges.cloudflare.com"]
238+ // 7. [data-sitekey]
239+
240+ setDocumentBody ( `
241+ <div>
242+ <iframe src="https://js.hcaptcha.com/a-b-c"></iframe>
243+ <div class="g-recaptcha" data-sitekey="google-key"></div>
244+ <div class="h-captcha" data-sitekey="hcaptcha-key"></div>
245+ </div>
246+ ` ) ;
247+
248+ mockVisibility ( 'iframe[src*="hcaptcha.com"]' , false ) ;
249+ mockVisibility ( '.g-recaptcha' , true ) ;
250+ mockVisibility ( '.h-captcha' , false ) ;
251+
252+ // It should skip the hidden hCaptcha iframe and find the visible .g-recaptcha
253+ expect ( detectCaptcha ( ) ) . toBe ( '.g-recaptcha' ) ;
254+ } ) ;
255+
256+ // --- REFACTORED TEST ---
257+ // Test for line 45 (non-module environment)
258+ test ( 'should execute detectCaptcha directly in non-module (browser) environment' , ( ) => {
259+ // 1. Spy on the document method *before* the script runs
260+ const querySelectorSpy = jest . spyOn ( document , 'querySelector' ) . mockImplementation ( ( ) => null ) ;
261+
262+ // 2. Save original module
263+ const originalModule = global . module ;
264+
265+ try {
266+ // 3. Read the script file content
267+ const scriptPath = path . resolve ( __dirname , 'captcha_detection.js' ) ;
268+ const scriptContent = fs . readFileSync ( scriptPath , 'utf8' ) ;
269+
270+ // 4. THE FIX: Shadow the test file's local 'module' variable
271+ const module = undefined ;
272+
273+ // 5. 'eval' the script content. This will run in the current
274+ // scope where 'module' is undefined, triggering the 'else' block.
275+ eval ( scriptContent ) ;
276+
277+ // 6. Check that the spy was called by the 'else' block's execution
278+ expect ( querySelectorSpy ) . toHaveBeenCalledWith ( 'iframe[src*="recaptcha/api"]' ) ;
279+
280+ } finally {
281+ // 7. Restore
282+ global . module = originalModule ; // Restore global
283+ querySelectorSpy . mockRestore ( ) ;
284+ jest . resetModules ( ) ; // Clean up
285+ }
286+ } ) ;
287+ } ) ;
0 commit comments