@@ -356,3 +356,244 @@ describe('[Issue] generateVersusSVG — zero existing test coverage', () => {
356356 expect ( svg ) . not . toContain ( 'prefers-color-scheme: dark' ) ;
357357 } ) ;
358358} ) ;
359+
360+ // ─── Custom gradient_stops and gradient_dir parameters ───────────────────────
361+
362+ describe ( '[Feature] custom gradient_stops and gradient_dir' , ( ) => {
363+ it ( 'existing gradient=true renders default gradient without custom stops' , ( ) => {
364+ const svg = generateSVG (
365+ baseStats ,
366+ {
367+ user : 'chetan' ,
368+ bg : hexColor ( '0d1117' ) ,
369+ text : hexColor ( 'ffffff' ) ,
370+ accent : hexColor ( 'ff00ff' ) ,
371+ speed : '8s' ,
372+ scale : 'linear' ,
373+ gradient : true ,
374+ } satisfies BadgeParams ,
375+ baseCalendar
376+ ) ;
377+
378+ // Should use default tower-grad-level-* IDs
379+ expect ( svg ) . toContain ( 'tower-grad-level-1' ) ;
380+ expect ( svg ) . toContain ( 'tower-grad-level-2' ) ;
381+ } ) ;
382+
383+ it ( 'gradient=true with valid gradient_stops renders custom gradient' , ( ) => {
384+ const svg = generateSVG (
385+ baseStats ,
386+ {
387+ user : 'chetan' ,
388+ bg : hexColor ( '0d1117' ) ,
389+ text : hexColor ( 'ffffff' ) ,
390+ accent : hexColor ( 'ff00ff' ) ,
391+ speed : '8s' ,
392+ scale : 'linear' ,
393+ gradient : true ,
394+ gradient_stops : 'ff6b35,ff007f,7000ff' ,
395+ } satisfies BadgeParams ,
396+ baseCalendar
397+ ) ;
398+
399+ // Should contain custom gradient colors
400+ expect ( svg ) . toContain ( '#ff6b35' ) ;
401+ expect ( svg ) . toContain ( '#ff007f' ) ;
402+ expect ( svg ) . toContain ( '#7000ff' ) ;
403+ // Should have custom gradient IDs, not default tower-grad-level-*
404+ expect ( svg ) . toContain ( 'custom-grad-' ) ;
405+ } ) ;
406+
407+ it ( 'gradient_stops with # prefix works correctly' , ( ) => {
408+ const svg = generateSVG (
409+ baseStats ,
410+ {
411+ user : 'chetan' ,
412+ bg : hexColor ( '0d1117' ) ,
413+ text : hexColor ( 'ffffff' ) ,
414+ accent : hexColor ( 'ff00ff' ) ,
415+ speed : '8s' ,
416+ scale : 'linear' ,
417+ gradient : true ,
418+ gradient_stops : '#ff6b35,#ff007f,#7000ff' ,
419+ } satisfies BadgeParams ,
420+ baseCalendar
421+ ) ;
422+
423+ // Should normalize and use the colors
424+ expect ( svg ) . toContain ( '#ff6b35' ) ;
425+ expect ( svg ) . toContain ( '#ff007f' ) ;
426+ expect ( svg ) . toContain ( '#7000ff' ) ;
427+ } ) ;
428+
429+ it ( 'invalid gradient_stops falls back to default gradient' , ( ) => {
430+ const svg = generateSVG (
431+ baseStats ,
432+ {
433+ user : 'chetan' ,
434+ bg : hexColor ( '0d1117' ) ,
435+ text : hexColor ( 'ffffff' ) ,
436+ accent : hexColor ( 'ff00ff' ) ,
437+ speed : '8s' ,
438+ scale : 'linear' ,
439+ gradient : true ,
440+ gradient_stops : 'invalid,colors,here' ,
441+ } satisfies BadgeParams ,
442+ baseCalendar
443+ ) ;
444+
445+ // Should fallback to default gradient (tower-grad-level-*)
446+ expect ( svg ) . toContain ( 'tower-grad-level-1' ) ;
447+ expect ( svg ) . not . toContain ( 'custom-grad-' ) ;
448+ } ) ;
449+
450+ it ( 'fewer than 2 valid colors in gradient_stops falls back to default' , ( ) => {
451+ const svg = generateSVG (
452+ baseStats ,
453+ {
454+ user : 'chetan' ,
455+ bg : hexColor ( '0d1117' ) ,
456+ text : hexColor ( 'ffffff' ) ,
457+ accent : hexColor ( 'ff00ff' ) ,
458+ speed : '8s' ,
459+ scale : 'linear' ,
460+ gradient : true ,
461+ gradient_stops : 'ff6b35' ,
462+ } satisfies BadgeParams ,
463+ baseCalendar
464+ ) ;
465+
466+ // Should fallback to default gradient
467+ expect ( svg ) . toContain ( 'tower-grad-level-1' ) ;
468+ expect ( svg ) . not . toContain ( 'custom-grad-' ) ;
469+ } ) ;
470+
471+ it ( 'gradient_dir=vertical produces correct SVG coordinates' , ( ) => {
472+ const svg = generateSVG (
473+ baseStats ,
474+ {
475+ user : 'chetan' ,
476+ bg : hexColor ( '0d1117' ) ,
477+ text : hexColor ( 'ffffff' ) ,
478+ accent : hexColor ( 'ff00ff' ) ,
479+ speed : '8s' ,
480+ scale : 'linear' ,
481+ gradient : true ,
482+ gradient_stops : 'ff6b35,7000ff' ,
483+ gradient_dir : 'vertical' ,
484+ } satisfies BadgeParams ,
485+ baseCalendar
486+ ) ;
487+
488+ // Vertical gradient should have y1 and y2 different (0% to 100%)
489+ expect ( svg ) . toMatch ( / x 1 = " 0 % " \s + y 1 = " 0 % " \s + x 2 = " 0 % " \s + y 2 = " 1 0 0 % " / ) ;
490+ } ) ;
491+
492+ it ( 'gradient_dir=horizontal produces correct SVG coordinates' , ( ) => {
493+ const svg = generateSVG (
494+ baseStats ,
495+ {
496+ user : 'chetan' ,
497+ bg : hexColor ( '0d1117' ) ,
498+ text : hexColor ( 'ffffff' ) ,
499+ accent : hexColor ( 'ff00ff' ) ,
500+ speed : '8s' ,
501+ scale : 'linear' ,
502+ gradient : true ,
503+ gradient_stops : 'ff6b35,7000ff' ,
504+ gradient_dir : 'horizontal' ,
505+ } satisfies BadgeParams ,
506+ baseCalendar
507+ ) ;
508+
509+ // Horizontal gradient should have x1 and x2 different (0% to 100%)
510+ expect ( svg ) . toMatch ( / x 1 = " 0 % " \s + y 1 = " 0 % " \s + x 2 = " 1 0 0 % " \s + y 2 = " 0 % " / ) ;
511+ } ) ;
512+
513+ it ( 'gradient_dir=diagonal produces correct SVG coordinates' , ( ) => {
514+ const svg = generateSVG (
515+ baseStats ,
516+ {
517+ user : 'chetan' ,
518+ bg : hexColor ( '0d1117' ) ,
519+ text : hexColor ( 'ffffff' ) ,
520+ accent : hexColor ( 'ff00ff' ) ,
521+ speed : '8s' ,
522+ scale : 'linear' ,
523+ gradient : true ,
524+ gradient_stops : 'ff6b35,7000ff' ,
525+ gradient_dir : 'diagonal' ,
526+ } satisfies BadgeParams ,
527+ baseCalendar
528+ ) ;
529+
530+ // Diagonal gradient should have both x and y varying
531+ expect ( svg ) . toMatch ( / x 1 = " 0 % " \s + y 1 = " 0 % " \s + x 2 = " 1 0 0 % " \s + y 2 = " 1 0 0 % " / ) ;
532+ } ) ;
533+
534+ it ( 'invalid gradient_dir falls back to vertical' , ( ) => {
535+ const svg = generateSVG (
536+ baseStats ,
537+ {
538+ user : 'chetan' ,
539+ bg : hexColor ( '0d1117' ) ,
540+ text : hexColor ( 'ffffff' ) ,
541+ accent : hexColor ( 'ff00ff' ) ,
542+ speed : '8s' ,
543+ scale : 'linear' ,
544+ gradient : true ,
545+ gradient_stops : 'ff6b35,7000ff' ,
546+ gradient_dir : 'invalid' ,
547+ } satisfies BadgeParams ,
548+ baseCalendar
549+ ) ;
550+
551+ // Should fallback to vertical
552+ expect ( svg ) . toMatch ( / x 1 = " 0 % " \s + y 1 = " 0 % " \s + x 2 = " 0 % " \s + y 2 = " 1 0 0 % " / ) ;
553+ } ) ;
554+
555+ it ( 'gradient_stops with mixed valid and invalid colors ignores invalid ones' , ( ) => {
556+ const svg = generateSVG (
557+ baseStats ,
558+ {
559+ user : 'chetan' ,
560+ bg : hexColor ( '0d1117' ) ,
561+ text : hexColor ( 'ffffff' ) ,
562+ accent : hexColor ( 'ff00ff' ) ,
563+ speed : '8s' ,
564+ scale : 'linear' ,
565+ gradient : true ,
566+ gradient_stops : 'ff6b35,invalid,7000ff' ,
567+ } satisfies BadgeParams ,
568+ baseCalendar
569+ ) ;
570+
571+ // Should use the 2 valid colors and ignore invalid
572+ expect ( svg ) . toContain ( '#ff6b35' ) ;
573+ expect ( svg ) . toContain ( '#7000ff' ) ;
574+ expect ( svg ) . toContain ( 'custom-grad-' ) ;
575+ } ) ;
576+
577+ it ( 'gradient=false ignores gradient_stops and gradient_dir' , ( ) => {
578+ const svg = generateSVG (
579+ baseStats ,
580+ {
581+ user : 'chetan' ,
582+ bg : hexColor ( '0d1117' ) ,
583+ text : hexColor ( 'ffffff' ) ,
584+ accent : hexColor ( 'ff00ff' ) ,
585+ speed : '8s' ,
586+ scale : 'linear' ,
587+ gradient : false ,
588+ gradient_stops : 'ff6b35,7000ff' ,
589+ gradient_dir : 'horizontal' ,
590+ } satisfies BadgeParams ,
591+ baseCalendar
592+ ) ;
593+
594+ // Should not contain any gradient definitions
595+ expect ( svg ) . not . toContain ( 'linearGradient' ) ;
596+ expect ( svg ) . not . toContain ( 'custom-grad-' ) ;
597+ expect ( svg ) . not . toContain ( 'tower-grad-level-' ) ;
598+ } ) ;
599+ } ) ;
0 commit comments