@@ -596,13 +596,16 @@ describe('Control', () => {
596596
597597 const result = await control . configureSlot ( 2 , 'PETG' , '#46328E' ) ;
598598
599+ // Creator 5: color must be an exact firmware palette match — uppercase
600+ // "#RRGGBB" WITH the leading "#". #46328E is off-palette and snaps to the
601+ // nearest C5 entry, Purple (#48358C). NOT the stripped AD5X value.
599602 expect ( result ) . toBe ( true ) ;
600603 expect ( mockedAxios . post ) . toHaveBeenCalledWith (
601604 `http://printer:8898${ Endpoints . Control } ` ,
602605 expect . objectContaining ( {
603606 payload : {
604607 cmd : Commands . MaterialStationConfigCmd ,
605- args : { slot : 2 , mt : 'PETG' , rgb : '46328E ' } ,
608+ args : { slot : 2 , mt : 'PETG' , rgb : '#48358C ' } ,
606609 } ,
607610 } ) ,
608611 expect . any ( Object )
@@ -620,6 +623,73 @@ describe('Control', () => {
620623 } ) ;
621624 } ) ;
622625
626+ // Creator 5 / Creator 5 Pro slot color: the firmware renders an icon ONLY on a
627+ // byte-for-byte, case-sensitive match against its 24-entry palette (WITH the
628+ // "#"). configureSlot must snap the caller's color to the nearest palette entry
629+ // and emit uppercase "#RRGGBB" — never an off-list or stripped value.
630+ describe ( 'Creator 5 material-station slot color (palette snapping)' , ( ) => {
631+ const paletteHexes = [
632+ '#FFFFFF' , '#FFF245' , '#DEF578' , '#21CC3D' , '#167A4B' , '#156682' , '#24E4A0' ,
633+ '#7BD9F0' , '#4CAAF8' , '#2E54DD' , '#48358C' , '#A341F7' , '#F435F6' , '#D5B4DE' ,
634+ '#FA6173' , '#F82D29' , '#805003' , '#F9903B' , '#FCEBD7' , '#D5C5A1' , '#B17C38' ,
635+ '#8C8C89' , '#BEBEBE' , '#1B1B1B' ,
636+ ] ;
637+
638+ const captureRgb = async ( hexRgb : string ) : Promise < string > => {
639+ mockedAxios . post . mockClear ( ) ;
640+ await control . configureSlot ( 1 , 'PLA' , hexRgb ) ;
641+ const call = mockedAxios . post . mock . calls [ 0 ] ;
642+ const payload = call [ 1 ] as { payload : { args : { rgb : string } } } ;
643+ return payload . payload . args . rgb ;
644+ } ;
645+
646+ beforeEach ( ( ) => {
647+ mockedAxios . post . mockResolvedValue ( { status : 200 , data : { code : 0 , message : 'Success' } } ) ;
648+ mockFiveMClient . isAD5X = false ;
649+ mockFiveMClient . isCreator5 = true ;
650+ } ) ;
651+
652+ it ( 'always snaps to a value from the 24-entry C5 palette (never off-list)' , async ( ) => {
653+ const inputs = [ '#FF0000' , '#123456' , '#00FF00' , '#ABCDEF' , '#A341F7' , '#112233' , '#FEDCBA' ] ;
654+ for ( const input of inputs ) {
655+ const rgb = await captureRgb ( input ) ;
656+ expect ( paletteHexes ) . toContain ( rgb ) ;
657+ }
658+ } ) ;
659+
660+ it ( 'always emits uppercase hex with a leading "#"' , async ( ) => {
661+ const rgb = await captureRgb ( '#ff0000' ) ;
662+ expect ( rgb . startsWith ( '#' ) ) . toBe ( true ) ;
663+ expect ( rgb ) . toBe ( rgb . toUpperCase ( ) ) ;
664+ expect ( rgb ) . toMatch ( / ^ # [ 0 - 9 A - F ] { 6 } $ / ) ;
665+ } ) ;
666+
667+ it ( 'snaps #FF0000 (pure red) to #F82D29 (nearest palette red)' , async ( ) => {
668+ expect ( await captureRgb ( '#FF0000' ) ) . toBe ( '#F82D29' ) ;
669+ } ) ;
670+
671+ it ( 'snaps an exact palette entry to itself (case-normalized)' , async ( ) => {
672+ expect ( await captureRgb ( '#4CAAF8' ) ) . toBe ( '#4CAAF8' ) ;
673+ expect ( await captureRgb ( '#4caaf8' ) ) . toBe ( '#4CAAF8' ) ;
674+ expect ( await captureRgb ( '4caaF8' ) ) . toBe ( '#4CAAF8' ) ;
675+ } ) ;
676+
677+ it ( 'snaps white to #FFFFFF' , async ( ) => {
678+ expect ( await captureRgb ( '#FFFFFF' ) ) . toBe ( '#FFFFFF' ) ;
679+ expect ( await captureRgb ( '#FFF' ) ) . toBe ( '#FFFFFF' ) ;
680+ } ) ;
681+
682+ // REGRESSION GUARD: the AD5X path must remain unchanged — freeform hex with
683+ // the leading "#" stripped, no palette snapping.
684+ it ( 'AD5X path is unchanged: strips "#" and keeps freeform hex (no snapping)' , async ( ) => {
685+ mockFiveMClient . isAD5X = true ;
686+ mockFiveMClient . isCreator5 = false ;
687+
688+ expect ( await captureRgb ( '#FF0000' ) ) . toBe ( 'FF0000' ) ;
689+ expect ( await captureRgb ( '#46328E' ) ) . toBe ( '46328E' ) ;
690+ } ) ;
691+ } ) ;
692+
623693 describe ( 'sendJobControlCmd' , ( ) => {
624694 it ( 'should send job control command' , async ( ) => {
625695 mockedAxios . post . mockResolvedValue ( {
0 commit comments