@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
22import {
33 scrapeLatestOfficialUnityVersion ,
44 scrapeVersions ,
5+ scrapeRecentOfficialUnityVersions ,
56} from '../src/logic/ingestUnityVersions/scrapeVersions' ;
67import { SearchMode } from 'unity-changeset' ;
78import fetch from 'node-fetch' ;
@@ -337,3 +338,199 @@ describe('scrapeVersions', () => {
337338 await expect ( scrapeVersions ( ) ) . rejects . toThrow ( 'No Unity versions found!' ) ;
338339 } ) ;
339340} ) ;
341+
342+ describe ( 'scrapeRecentOfficialUnityVersions' , ( ) => {
343+ beforeEach ( ( ) => {
344+ vi . clearAllMocks ( ) ;
345+ } ) ;
346+
347+ it ( 'should discover multiple recent versions from the releases page' , async ( ) => {
348+ const html = `
349+ <h1>Unity 6000.4.10f1</h1>
350+ <a href="unityhub://6000.4.10f1/feeafc12a938">Install</a>
351+
352+ <h2>Unity 6000.3.17f1</h2>
353+ <p>Changeset: abc123def456</p>
354+
355+ <h2>Unity 6000.2.5f1</h2>
356+ <a href="unityhub://6000.2.5f1/xyz789uvw123">Download</a>
357+ ` ;
358+ mockedFetch . mockResolvedValue ( {
359+ ok : true ,
360+ status : 200 ,
361+ text : async ( ) => html ,
362+ } as any ) ;
363+
364+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
365+
366+ expect ( result ) . toHaveLength ( 3 ) ;
367+ expect ( result . map ( ( v ) => v . version ) ) . toContain ( '6000.4.10f1' ) ;
368+ expect ( result . map ( ( v ) => v . version ) ) . toContain ( '6000.3.17f1' ) ;
369+ expect ( result . map ( ( v ) => v . version ) ) . toContain ( '6000.2.5f1' ) ;
370+ } ) ;
371+
372+ it ( 'should extract changesets from unityhub:// URLs' , async ( ) => {
373+ const html = `
374+ <a href="unityhub://6000.4.10f1/feeafc12a938">Install</a>
375+ <a href="unityhub://6000.3.17f1/abc123def456">Install</a>
376+ ` ;
377+ mockedFetch . mockResolvedValue ( {
378+ ok : true ,
379+ status : 200 ,
380+ text : async ( ) => html ,
381+ } as any ) ;
382+
383+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
384+
385+ expect ( result ) . toContainEqual (
386+ expect . objectContaining ( {
387+ version : '6000.4.10f1' ,
388+ changeSet : 'feeafc12a938' ,
389+ } ) ,
390+ ) ;
391+ expect ( result ) . toContainEqual (
392+ expect . objectContaining ( {
393+ version : '6000.3.17f1' ,
394+ changeSet : 'abc123def456' ,
395+ } ) ,
396+ ) ;
397+ } ) ;
398+
399+ it ( 'should extract changesets from "Changeset:" markers' , async ( ) => {
400+ const html = `
401+ <h2>Unity 6000.4.10f1</h2>
402+ <p>Changeset: feeafc12a938</p>
403+
404+ <h2>Unity 6000.3.17f1</h2>
405+ <div>Version Changeset: abc123def456</div>
406+ ` ;
407+ mockedFetch . mockResolvedValue ( {
408+ ok : true ,
409+ status : 200 ,
410+ text : async ( ) => html ,
411+ } as any ) ;
412+
413+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
414+
415+ expect ( result ) . toContainEqual (
416+ expect . objectContaining ( {
417+ version : '6000.4.10f1' ,
418+ changeSet : 'feeafc12a938' ,
419+ } ) ,
420+ ) ;
421+ expect ( result ) . toContainEqual (
422+ expect . objectContaining ( {
423+ version : '6000.3.17f1' ,
424+ changeSet : 'abc123def456' ,
425+ } ) ,
426+ ) ;
427+ } ) ;
428+
429+ it ( 'should skip versions without valid changesets' , async ( ) => {
430+ const html = `
431+ <h2>Unity 6000.4.10f1</h2>
432+ <a href="unityhub://6000.4.10f1/feeafc12a938">Install</a>
433+
434+ <h2>Unity 6000.3.17f1</h2>
435+ <!-- No changeset provided -->
436+
437+ <h2>Unity 6000.2.5f1</h2>
438+ <p>Changeset: xyz789uvw123</p>
439+ ` ;
440+ mockedFetch . mockResolvedValue ( {
441+ ok : true ,
442+ status : 200 ,
443+ text : async ( ) => html ,
444+ } as any ) ;
445+
446+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
447+
448+ // Only versions with valid changesets should be included
449+ expect ( result ) . toHaveLength ( 2 ) ;
450+ expect ( result . map ( ( v ) => v . version ) ) . not . toContain ( '6000.3.17f1' ) ;
451+ } ) ;
452+
453+ it ( 'should deduplicate versions found multiple times on the page' , async ( ) => {
454+ const html = `
455+ <h2>Unity 6000.4.10f1</h2>
456+ <a href="unityhub://6000.4.10f1/feeafc12a938">Install</a>
457+
458+ <p>Latest version: 6000.4.10f1</p>
459+ <a href="unityhub://6000.4.10f1/feeafc12a938">Download</a>
460+ ` ;
461+ mockedFetch . mockResolvedValue ( {
462+ ok : true ,
463+ status : 200 ,
464+ text : async ( ) => html ,
465+ } as any ) ;
466+
467+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
468+
469+ expect ( result ) . toHaveLength ( 1 ) ;
470+ expect ( result [ 0 ] . version ) . toBe ( '6000.4.10f1' ) ;
471+ } ) ;
472+
473+ it ( 'should return empty array if page returns error' , async ( ) => {
474+ mockedFetch . mockResolvedValue ( {
475+ ok : false ,
476+ status : 404 ,
477+ } as any ) ;
478+
479+ await expect ( scrapeRecentOfficialUnityVersions ( ) ) . rejects . toThrow (
480+ 'Unity release page returned 404' ,
481+ ) ;
482+ } ) ;
483+
484+ it ( 'should filter out non-final versions' , async ( ) => {
485+ const html = `
486+ <h2>Unity 6000.4.10f1</h2>
487+ <a href="unityhub://6000.4.10f1/feeafc12a938">Install</a>
488+
489+ <h2>Unity 6000.4.10a1</h2>
490+ <a href="unityhub://6000.4.10a1/abc123456789">Install</a>
491+
492+ <h2>Unity 6000.4.9f1</h2>
493+ <p>Changeset: xyz789uvw123</p>
494+ ` ;
495+ mockedFetch . mockResolvedValue ( {
496+ ok : true ,
497+ status : 200 ,
498+ text : async ( ) => html ,
499+ } as any ) ;
500+
501+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
502+
503+ expect ( result ) . toHaveLength ( 2 ) ;
504+ expect ( result . map ( ( v ) => v . version ) ) . toContain ( '6000.4.10f1' ) ;
505+ expect ( result . map ( ( v ) => v . version ) ) . toContain ( '6000.4.9f1' ) ;
506+ expect ( result . map ( ( v ) => v . version ) ) . not . toContain ( '6000.4.10a1' ) ;
507+ } ) ;
508+
509+ describe ( 'integration test (live)' , ( ) => {
510+ it . skipIf ( process . env . CI === 'false' ) ( 'should successfully scrape real Unity releases page' , async ( ) => {
511+ // This test runs in CI only and validates the scraping logic against the real page
512+ const realFetch = ( await import ( 'node-fetch' ) ) . default ;
513+ vi . mocked ( fetch ) . mockImplementation ( realFetch as any ) ;
514+
515+ const result = await scrapeRecentOfficialUnityVersions ( ) ;
516+
517+ // Should find at least some recent versions
518+ expect ( result . length ) . toBeGreaterThan ( 0 ) ;
519+
520+ // All versions should be valid
521+ result . forEach ( ( version ) => {
522+ expect ( version . version ) . toMatch ( / ^ \d + \. \d + \. \d + f \d + $ / ) ;
523+ expect ( version . changeSet ) . toMatch ( / ^ [ a - f 0 - 9 ] { 12 } $ / ) ;
524+ expect ( version . major ) . toBeGreaterThanOrEqual ( 2017 ) ;
525+ } ) ;
526+
527+ console . log ( `Found ${ result . length } recent versions on Unity releases page` ) ;
528+ console . log (
529+ `Latest versions: ${ result
530+ . slice ( 0 , 5 )
531+ . map ( ( v ) => v . version )
532+ . join ( ', ' ) } `,
533+ ) ;
534+ } ) ;
535+ } ) ;
536+ } ) ;
0 commit comments