/ src / browser / stealth.test.ts
stealth.test.ts
  1  import { describe, it, expect } from 'vitest';
  2  import { generateStealthJs } from './stealth.js';
  3  
  4  /**
  5   * Tests for the stealth anti-detection module.
  6   *
  7   * We test the generated JS string for expected content and structure.
  8   * Evaluating in Node is fragile because stealth patches target browser
  9   * globals (navigator, Performance, HTMLIFrameElement) that don't exist
 10   * or behave differently in Node. Instead we verify the code string
 11   * contains the right patches and is syntactically valid.
 12   */
 13  
 14  describe('generateStealthJs', () => {
 15    it('returns a non-empty string', () => {
 16      const code = generateStealthJs();
 17      expect(typeof code).toBe('string');
 18      expect(code.length).toBeGreaterThan(0);
 19    });
 20  
 21    it('is a valid self-contained IIFE', () => {
 22      const code = generateStealthJs();
 23      // Should start/end as an IIFE
 24      expect(code.trim()).toMatch(/^\(\(\) => \{/);
 25      expect(code.trim()).toMatch(/\}\)\(\)$/);
 26    });
 27  
 28    it('patches navigator.webdriver', () => {
 29      const code = generateStealthJs();
 30      expect(code).toContain("navigator, 'webdriver'");
 31      expect(code).toContain('() => false');
 32    });
 33  
 34    it('stubs window.chrome', () => {
 35      const code = generateStealthJs();
 36      expect(code).toContain('window.chrome');
 37      expect(code).toContain('runtime');
 38      expect(code).toContain('loadTimes');
 39      expect(code).toContain('csi');
 40    });
 41  
 42    it('fakes navigator.plugins if empty', () => {
 43      const code = generateStealthJs();
 44      expect(code).toContain('navigator.plugins');
 45      expect(code).toContain('PDF Viewer');
 46      expect(code).toContain('Chrome PDF Viewer');
 47    });
 48  
 49    it('ensures navigator.languages is non-empty', () => {
 50      const code = generateStealthJs();
 51      expect(code).toContain('navigator.languages');
 52      expect(code).toContain("'en-US'");
 53    });
 54  
 55    it('normalizes Permissions.query for notifications', () => {
 56      const code = generateStealthJs();
 57      expect(code).toContain('Permissions');
 58      expect(code).toContain('notifications');
 59    });
 60  
 61    it('cleans automation artifacts', () => {
 62      const code = generateStealthJs();
 63      expect(code).toContain('__playwright');
 64      expect(code).toContain('__puppeteer');
 65      expect(code).toContain("'cdc_'");
 66      expect(code).toContain("'__cdc_'");
 67    });
 68  
 69    it('filters CDP patterns from Error.stack', () => {
 70      const code = generateStealthJs();
 71      expect(code).toContain('puppeteer_evaluation_script');
 72      expect(code).toContain("'pptr:'");
 73      expect(code).toContain("'debugger://'");
 74    });
 75  
 76    it('neutralizes debugger statement traps', () => {
 77      const code = generateStealthJs();
 78      // Should patch Function constructor with new.target / Reflect.construct
 79      expect(code).toContain('_OrigFunction');
 80      expect(code).toContain('_PatchedFunction');
 81      expect(code).toContain('new.target');
 82      expect(code).toContain('Reflect.construct');
 83      // Should patch eval
 84      expect(code).toContain('_origEval');
 85      expect(code).toContain('_patchedEval');
 86      // Regex to strip debugger (lookbehind for statement boundaries)
 87      expect(code).toContain('_debuggerRe');
 88    });
 89  
 90    it('uses shared toString disguise via WeakMap', () => {
 91      const code = generateStealthJs();
 92      // Shared infrastructure at the top of the IIFE
 93      expect(code).toContain('_origToString');
 94      expect(code).toContain('WeakMap');
 95      expect(code).toContain('_disguised');
 96      expect(code).toContain('_disguise');
 97      // Should NOT have per-instance toString overrides on Function/eval
 98      // (they go through _disguise instead)
 99    });
100  
101    it('defends console method fingerprinting', () => {
102      const code = generateStealthJs();
103      expect(code).toContain('_consoleMethods');
104      expect(code).toContain("'log'");
105      expect(code).toContain("'warn'");
106      expect(code).toContain("'error'");
107      expect(code).toContain('[native code]');
108      // Uses saved _origToString reference
109      expect(code).toContain('_origToString.call');
110    });
111  
112    it('defends window dimension detection', () => {
113      const code = generateStealthJs();
114      expect(code).toContain('outerWidth');
115      expect(code).toContain('outerHeight');
116      expect(code).toContain('innerWidth');
117      expect(code).toContain('innerHeight');
118    });
119  
120    it('filters Performance API entries', () => {
121      const code = generateStealthJs();
122      expect(code).toContain('getEntries');
123      expect(code).toContain('getEntriesByType');
124      expect(code).toContain('getEntriesByName');
125      expect(code).toContain('_suspiciousPatterns');
126    });
127  
128    it('cleans document $cdc_ properties', () => {
129      const code = generateStealthJs();
130      expect(code).toContain("'$cdc_'");
131      expect(code).toContain("'$chrome_'");
132    });
133  
134    it('patches iframe contentWindow.chrome consistency', () => {
135      const code = generateStealthJs();
136      expect(code).toContain('contentWindow');
137      expect(code).toContain('HTMLIFrameElement');
138    });
139  
140    it('uses non-enumerable guard flag on EventTarget.prototype', () => {
141      const code = generateStealthJs();
142      expect(code).toContain('EventTarget.prototype');
143      expect(code).toContain("'__lsn'");
144      expect(code).toContain('enumerable: false');
145    });
146  
147    it('generates syntactically valid JavaScript', () => {
148      const code = generateStealthJs();
149      // new Function() parses the code without executing it in a real
150      // browser context, catching syntax errors from template literal issues.
151      expect(() => new Function(code)).not.toThrow();
152    });
153  });