cross-platform.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /**
  2. * @fileoverview Cross-Platform Compatibility Tests
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. import { describe, it, expect, beforeEach, vi } from 'vitest';
  8. import os from 'os';
  9. import path from 'path';
  10. import fs from 'fs';
  11. /**
  12. * CROSS-PLATFORM COMPATIBILITY TESTS
  13. *
  14. * These tests verify that the application works correctly across:
  15. * - macOS (darwin)
  16. * - Windows (win32)
  17. * - Linux (linux)
  18. *
  19. * Testing areas:
  20. * - Binary path resolution
  21. * - File system operations
  22. * - Path handling
  23. * - Process spawning
  24. * - Platform-specific features
  25. */
  26. describe('Cross-Platform Compatibility', () => {
  27. let currentPlatform;
  28. let mockPlatformInfo;
  29. beforeEach(() => {
  30. currentPlatform = process.platform;
  31. mockPlatformInfo = {
  32. platform: currentPlatform,
  33. arch: process.arch,
  34. homedir: os.homedir(),
  35. tmpdir: os.tmpdir(),
  36. pathSep: path.sep
  37. };
  38. });
  39. describe('Platform Detection and Binary Paths', () => {
  40. it('should detect current platform correctly', () => {
  41. const supportedPlatforms = ['darwin', 'win32', 'linux'];
  42. expect(supportedPlatforms).toContain(currentPlatform);
  43. });
  44. it('should resolve correct binary paths for each platform', () => {
  45. const getBinaryPath = (binaryName, platform = currentPlatform) => {
  46. const extension = platform === 'win32' ? '.exe' : '';
  47. return path.join('binaries', `${binaryName}${extension}`);
  48. };
  49. // Test for current platform
  50. const ytDlpPath = getBinaryPath('yt-dlp');
  51. const ffmpegPath = getBinaryPath('ffmpeg');
  52. if (currentPlatform === 'win32') {
  53. expect(ytDlpPath).toBe('binaries\\yt-dlp.exe');
  54. expect(ffmpegPath).toBe('binaries\\ffmpeg.exe');
  55. } else {
  56. expect(ytDlpPath).toBe('binaries/yt-dlp');
  57. expect(ffmpegPath).toBe('binaries/ffmpeg');
  58. }
  59. // Test for all platforms
  60. expect(getBinaryPath('yt-dlp', 'win32')).toMatch(/\.exe$/);
  61. expect(getBinaryPath('yt-dlp', 'darwin')).not.toMatch(/\.exe$/);
  62. expect(getBinaryPath('yt-dlp', 'linux')).not.toMatch(/\.exe$/);
  63. });
  64. it('should handle platform-specific path separators', () => {
  65. const createPath = (...segments) => path.join(...segments);
  66. const testPath = createPath('downloads', 'videos', 'test.mp4');
  67. if (currentPlatform === 'win32') {
  68. expect(testPath).toMatch(/\\/);
  69. } else {
  70. expect(testPath).toMatch(/\//);
  71. }
  72. });
  73. it('should resolve home directory correctly on all platforms', () => {
  74. const homeDir = os.homedir();
  75. expect(homeDir).toBeTruthy();
  76. expect(path.isAbsolute(homeDir)).toBe(true);
  77. // Platform-specific home directory patterns
  78. if (currentPlatform === 'win32') {
  79. expect(homeDir).toMatch(/^[A-Z]:\\/);
  80. } else {
  81. expect(homeDir).toMatch(/^\/.*\/[^/]+$/);
  82. }
  83. });
  84. });
  85. describe('File System Operations', () => {
  86. it('should handle file paths with different separators', () => {
  87. // Use path.join for cross-platform compatibility
  88. const testPaths = [
  89. path.join('downloads', 'video.mp4'),
  90. path.join('downloads', 'subfolder', 'video.mp4')
  91. ];
  92. testPaths.forEach(testPath => {
  93. const normalized = path.normalize(testPath);
  94. expect(normalized).toBeTruthy();
  95. const parsed = path.parse(normalized);
  96. expect(parsed.name).toBe('video');
  97. expect(parsed.ext).toBe('.mp4');
  98. // The base name should be consistent regardless of path separators
  99. expect(parsed.base).toBe('video.mp4');
  100. // Verify the path contains the expected directory
  101. expect(normalized).toMatch(/downloads/);
  102. });
  103. });
  104. it('should create directories with correct permissions', () => {
  105. const testDir = path.join(os.tmpdir(), 'grabzilla-test-' + Date.now());
  106. try {
  107. fs.mkdirSync(testDir, { recursive: true });
  108. expect(fs.existsSync(testDir)).toBe(true);
  109. const stats = fs.statSync(testDir);
  110. expect(stats.isDirectory()).toBe(true);
  111. // Check permissions (Unix-like systems)
  112. if (currentPlatform !== 'win32') {
  113. expect(stats.mode & 0o777).toBeGreaterThan(0);
  114. }
  115. } finally {
  116. // Cleanup
  117. if (fs.existsSync(testDir)) {
  118. fs.rmSync(testDir, { recursive: true, force: true });
  119. }
  120. }
  121. });
  122. it('should handle long file paths appropriately', () => {
  123. const longFileName = 'a'.repeat(200) + '.mp4';
  124. const longPath = path.join(os.tmpdir(), longFileName);
  125. // Windows has path length limitations
  126. if (currentPlatform === 'win32') {
  127. expect(longPath.length).toBeLessThan(260); // Windows MAX_PATH
  128. }
  129. // Test path parsing with long names
  130. const parsed = path.parse(longPath);
  131. expect(parsed.ext).toBe('.mp4');
  132. });
  133. it('should handle special characters in file names', () => {
  134. const specialChars = {
  135. 'win32': ['<', '>', ':', '"', '|', '?', '*'],
  136. 'darwin': [':'],
  137. 'linux': []
  138. };
  139. const invalidChars = specialChars[currentPlatform] || [];
  140. const testFileName = 'test_video_with_special_chars.mp4';
  141. // Verify our test filename doesn't contain invalid characters
  142. invalidChars.forEach(char => {
  143. expect(testFileName).not.toContain(char);
  144. });
  145. // Test sanitization function
  146. const sanitizeFileName = (name) => {
  147. let sanitized = name;
  148. invalidChars.forEach(char => {
  149. sanitized = sanitized.replace(new RegExp(`\\${char}`, 'g'), '_');
  150. });
  151. return sanitized;
  152. };
  153. const dirtyName = 'test<video>file.mp4';
  154. const cleanName = sanitizeFileName(dirtyName);
  155. if (currentPlatform === 'win32') {
  156. expect(cleanName).toBe('test_video_file.mp4');
  157. } else {
  158. expect(cleanName).toBe(dirtyName); // No changes needed on Unix-like systems
  159. }
  160. });
  161. });
  162. describe('Process Management', () => {
  163. it('should handle process spawning correctly on all platforms', () => {
  164. const getShellCommand = () => {
  165. switch (currentPlatform) {
  166. case 'win32':
  167. return { cmd: 'cmd', args: ['/c', 'echo', 'test'] };
  168. default:
  169. return { cmd: 'echo', args: ['test'] };
  170. }
  171. };
  172. const { cmd, args } = getShellCommand();
  173. expect(cmd).toBeTruthy();
  174. expect(Array.isArray(args)).toBe(true);
  175. });
  176. it('should handle process termination signals correctly', () => {
  177. const getTerminationSignal = () => {
  178. return currentPlatform === 'win32' ? 'SIGTERM' : 'SIGTERM';
  179. };
  180. const signal = getTerminationSignal();
  181. expect(['SIGTERM', 'SIGKILL', 'SIGINT']).toContain(signal);
  182. });
  183. it('should handle environment variables correctly', () => {
  184. const pathVar = process.env.PATH || process.env.Path;
  185. expect(pathVar).toBeTruthy();
  186. const pathSeparator = currentPlatform === 'win32' ? ';' : ':';
  187. const paths = pathVar.split(pathSeparator);
  188. expect(paths.length).toBeGreaterThan(0);
  189. });
  190. });
  191. describe('Platform-Specific Features', () => {
  192. it('should handle macOS-specific features', () => {
  193. if (currentPlatform === 'darwin') {
  194. // Test macOS-specific paths
  195. const applicationsPath = '/Applications';
  196. expect(fs.existsSync(applicationsPath)).toBe(true);
  197. // Test bundle handling
  198. const bundleExtensions = ['.app', '.bundle'];
  199. bundleExtensions.forEach(ext => {
  200. expect(ext.startsWith('.')).toBe(true);
  201. });
  202. } else {
  203. // Skip macOS-specific tests on other platforms
  204. expect(currentPlatform).not.toBe('darwin');
  205. }
  206. });
  207. it('should handle Windows-specific features', () => {
  208. if (currentPlatform === 'win32') {
  209. // Test Windows-specific paths
  210. const systemRoot = process.env.SystemRoot;
  211. expect(systemRoot).toBeTruthy();
  212. expect(systemRoot).toMatch(/^[A-Z]:\\/);
  213. // Test executable extensions
  214. const executableExtensions = ['.exe', '.bat', '.cmd'];
  215. executableExtensions.forEach(ext => {
  216. expect(ext.startsWith('.')).toBe(true);
  217. });
  218. } else {
  219. // Skip Windows-specific tests on other platforms
  220. expect(currentPlatform).not.toBe('win32');
  221. }
  222. });
  223. it('should handle Linux-specific features', () => {
  224. if (currentPlatform === 'linux') {
  225. // Test Linux-specific paths
  226. const homeDir = os.homedir();
  227. expect(homeDir).toMatch(/^\/home\/|^\/root$/);
  228. // Test executable permissions
  229. const executableMode = 0o755;
  230. expect(executableMode & 0o111).toBeGreaterThan(0); // Execute permissions
  231. } else {
  232. // Skip Linux-specific tests on other platforms
  233. expect(currentPlatform).not.toBe('linux');
  234. }
  235. });
  236. });
  237. describe('File Dialog Integration', () => {
  238. it('should provide platform-appropriate file dialog options', () => {
  239. const getFileDialogOptions = (type) => {
  240. const baseOptions = {
  241. title: 'Select File',
  242. buttonLabel: 'Select'
  243. };
  244. switch (type) {
  245. case 'save':
  246. return {
  247. ...baseOptions,
  248. defaultPath: path.join(os.homedir(), 'Downloads'),
  249. filters: [
  250. { name: 'Video Files', extensions: ['mp4', 'mkv', 'avi'] },
  251. { name: 'All Files', extensions: ['*'] }
  252. ]
  253. };
  254. case 'cookie':
  255. return {
  256. ...baseOptions,
  257. filters: [
  258. { name: 'Text Files', extensions: ['txt'] },
  259. { name: 'All Files', extensions: ['*'] }
  260. ]
  261. };
  262. default:
  263. return baseOptions;
  264. }
  265. };
  266. const saveOptions = getFileDialogOptions('save');
  267. const cookieOptions = getFileDialogOptions('cookie');
  268. expect(saveOptions.defaultPath).toContain(os.homedir());
  269. expect(saveOptions.filters).toHaveLength(2);
  270. expect(cookieOptions.filters).toHaveLength(2);
  271. });
  272. it('should handle default download directories per platform', () => {
  273. const getDefaultDownloadPath = () => {
  274. const homeDir = os.homedir();
  275. switch (currentPlatform) {
  276. case 'win32':
  277. return path.join(homeDir, 'Downloads');
  278. case 'darwin':
  279. return path.join(homeDir, 'Downloads');
  280. case 'linux':
  281. return path.join(homeDir, 'Downloads');
  282. default:
  283. return homeDir;
  284. }
  285. };
  286. const downloadPath = getDefaultDownloadPath();
  287. expect(path.isAbsolute(downloadPath)).toBe(true);
  288. expect(downloadPath).toContain(os.homedir());
  289. });
  290. });
  291. describe('Notification System', () => {
  292. it('should provide platform-appropriate notification options', () => {
  293. const getNotificationOptions = (title, body) => {
  294. const baseOptions = {
  295. title,
  296. body,
  297. silent: false
  298. };
  299. switch (currentPlatform) {
  300. case 'win32':
  301. return {
  302. ...baseOptions,
  303. toastXml: null // Windows-specific toast XML
  304. };
  305. case 'darwin':
  306. return {
  307. ...baseOptions,
  308. sound: 'default' // macOS notification sound
  309. };
  310. case 'linux':
  311. return {
  312. ...baseOptions,
  313. urgency: 'normal' // Linux notification urgency
  314. };
  315. default:
  316. return baseOptions;
  317. }
  318. };
  319. const options = getNotificationOptions('Test', 'Test notification');
  320. expect(options.title).toBe('Test');
  321. expect(options.body).toBe('Test notification');
  322. // Platform-specific properties
  323. if (currentPlatform === 'win32') {
  324. expect(options).toHaveProperty('toastXml');
  325. } else if (currentPlatform === 'darwin') {
  326. expect(options).toHaveProperty('sound');
  327. } else if (currentPlatform === 'linux') {
  328. expect(options).toHaveProperty('urgency');
  329. }
  330. });
  331. });
  332. describe('Performance Characteristics', () => {
  333. it('should account for platform-specific performance differences', () => {
  334. const performanceMetrics = {
  335. startupTime: 0,
  336. memoryUsage: process.memoryUsage(),
  337. cpuUsage: process.cpuUsage()
  338. };
  339. // Simulate startup time measurement
  340. const startTime = Date.now();
  341. // Simulate some work
  342. for (let i = 0; i < 1000000; i++) {
  343. Math.random();
  344. }
  345. performanceMetrics.startupTime = Date.now() - startTime;
  346. expect(performanceMetrics.startupTime).toBeGreaterThan(0);
  347. expect(performanceMetrics.memoryUsage.heapUsed).toBeGreaterThan(0);
  348. expect(performanceMetrics.cpuUsage.user).toBeGreaterThanOrEqual(0);
  349. });
  350. it('should handle concurrent operations efficiently per platform', () => {
  351. const maxConcurrency = currentPlatform === 'win32' ? 2 : 3; // Windows might be more conservative
  352. expect(maxConcurrency).toBeGreaterThan(0);
  353. expect(maxConcurrency).toBeLessThanOrEqual(4);
  354. });
  355. });
  356. describe('Error Handling and Recovery', () => {
  357. it('should provide platform-specific error messages', () => {
  358. const getErrorMessage = (errorType) => {
  359. const messages = {
  360. 'file_not_found': {
  361. 'win32': 'The system cannot find the file specified.',
  362. 'darwin': 'No such file or directory',
  363. 'linux': 'No such file or directory'
  364. },
  365. 'permission_denied': {
  366. 'win32': 'Access is denied.',
  367. 'darwin': 'Permission denied',
  368. 'linux': 'Permission denied'
  369. }
  370. };
  371. return messages[errorType]?.[currentPlatform] || 'Unknown error';
  372. };
  373. const fileNotFoundMsg = getErrorMessage('file_not_found');
  374. const permissionDeniedMsg = getErrorMessage('permission_denied');
  375. expect(fileNotFoundMsg).toBeTruthy();
  376. expect(permissionDeniedMsg).toBeTruthy();
  377. if (currentPlatform === 'win32') {
  378. expect(fileNotFoundMsg).toContain('system cannot find');
  379. expect(permissionDeniedMsg).toContain('Access is denied');
  380. } else {
  381. expect(fileNotFoundMsg).toContain('No such file');
  382. expect(permissionDeniedMsg).toContain('Permission denied');
  383. }
  384. });
  385. it('should handle platform-specific recovery strategies', () => {
  386. const getRecoveryStrategy = (errorType) => {
  387. switch (errorType) {
  388. case 'binary_not_found':
  389. return currentPlatform === 'win32'
  390. ? 'Check PATH environment variable and .exe extension'
  391. : 'Check PATH and executable permissions';
  392. case 'network_error':
  393. return 'Check internet connection and firewall settings';
  394. default:
  395. return 'Try restarting the application';
  396. }
  397. };
  398. const binaryStrategy = getRecoveryStrategy('binary_not_found');
  399. const networkStrategy = getRecoveryStrategy('network_error');
  400. expect(binaryStrategy).toBeTruthy();
  401. expect(networkStrategy).toBeTruthy();
  402. if (currentPlatform === 'win32') {
  403. expect(binaryStrategy).toContain('.exe');
  404. } else {
  405. expect(binaryStrategy).toContain('permissions');
  406. }
  407. });
  408. });
  409. });