@@ -75,6 +75,26 @@ function hypotheticalQualifiedV060Release(): FirmwareReleaseDescriptor {
7575 return release as FirmwareReleaseDescriptor ;
7676}
7777
78+ function candidateV060Release ( ) : FirmwareReleaseDescriptor {
79+ const release = hypotheticalQualifiedV060Release ( ) as unknown as {
80+ deployment : string ;
81+ accessControlled : boolean ;
82+ hilStatus : string ;
83+ } ;
84+ release . deployment = "candidate" ;
85+ release . accessControlled = true ;
86+ release . hilStatus = "pending" ;
87+ return release as unknown as FirmwareReleaseDescriptor ;
88+ }
89+
90+ const fiveProfileOrder = [
91+ "esp32-4mb" ,
92+ "esp32-s3-n16r8" ,
93+ "waveshare-esp32-s3-lcd-147b" ,
94+ "esp32-c3-4mb" ,
95+ "rpi-pico2-w" ,
96+ ] as const ;
97+
7898function jpegDimensions ( bytes : Buffer ) : { width : number ; height : number } {
7999 if ( bytes . length < 4 || bytes [ 0 ] !== 0xff || bytes [ 1 ] !== 0xd8 ) {
80100 throw new Error ( "not a JPEG" ) ;
@@ -706,7 +726,36 @@ describe("public-site contract", () => {
706726 status : "Planned · installer unavailable pending exact-profile HIL" ,
707727 planned : true ,
708728 } ,
729+ {
730+ id : "rpi-pico2-w" ,
731+ target : "Raspberry Pi Pico 2 W" ,
732+ constraint : "RP2350 + CYW43439 · UF2 / BOOTSEL" ,
733+ status : "Planned · installer unavailable pending exact-profile HIL" ,
734+ planned : true ,
735+ } ,
709736 ] ) ;
737+ expect (
738+ firmwareTargetsForRelease ( candidateV060Release ( ) ) . map (
739+ ( { id, planned, status } ) => ( { id, planned, status } ) ,
740+ ) ,
741+ ) . toEqual (
742+ fiveProfileOrder . map ( ( id ) => ( {
743+ id,
744+ planned : false ,
745+ status : "v0.6.0 protected candidate · HIL pending" ,
746+ } ) ) ,
747+ ) ;
748+ expect (
749+ firmwareTargetsForRelease ( hypotheticalQualifiedV060Release ( ) ) . map (
750+ ( { id, planned, status } ) => ( { id, planned, status } ) ,
751+ ) ,
752+ ) . toEqual (
753+ fiveProfileOrder . map ( ( id ) => ( {
754+ id,
755+ planned : false ,
756+ status : "v0.6.0 qualified public release" ,
757+ } ) ) ,
758+ ) ;
710759 expect (
711760 firmwareTargetsForRelease ( publicBetaFirmwareRelease )
712761 . filter ( ( { planned } ) => ! planned )
@@ -942,9 +991,90 @@ describe("public-site contract", () => {
942991 ) . toBeInTheDocument ( ) ;
943992 candidateHome . unmount ( ) ;
944993 render ( < SupportPage /> ) ;
994+ // The support page must mirror the staged candidate instead of
995+ // contradicting /flash with an installer-unavailable claim.
996+ expect (
997+ screen . getByText ( / p r o t e c t e d c a n d i d a t e v 0 \. 5 \. 1 i s s t a g e d / i) ,
998+ ) . toBeInTheDocument ( ) ;
999+ expect (
1000+ screen . queryByText ( / f i r m w a r e i n s t a l l e r i s c u r r e n t l y u n a v a i l a b l e / i) ,
1001+ ) . toBeNull ( ) ;
1002+ } finally {
1003+ if ( previousSelection === undefined ) {
1004+ delete process . env . PYBLE_FLASH_SELECTION_FILE ;
1005+ } else {
1006+ process . env . PYBLE_FLASH_SELECTION_FILE = previousSelection ;
1007+ }
1008+ await rm ( selectionRoot , { recursive : true , force : true } ) ;
1009+ }
1010+ } ) ;
1011+
1012+ it ( "presents all five v0.6.0 targets and the board photo for the staged candidate" , async ( ) => {
1013+ const selectionRoot = await mkdtemp (
1014+ join ( tmpdir ( ) , "pyble-site-v060-candidate-selection-" ) ,
1015+ ) ;
1016+ const selectionFile = join ( selectionRoot , "selection.json" ) ;
1017+ const previousSelection = process . env . PYBLE_FLASH_SELECTION_FILE ;
1018+ process . env . PYBLE_FLASH_SELECTION_FILE = selectionFile ;
1019+
1020+ try {
1021+ await writeFile (
1022+ selectionFile ,
1023+ JSON . stringify ( candidateV060Release ( ) ) ,
1024+ "utf8" ,
1025+ ) ;
1026+ const home = render ( < HomePage /> ) ;
1027+
1028+ // The hero reflects the five-target candidate instead of the stale
1029+ // pre-v0.6.0 C3/Pico unavailability sentence.
1030+ expect (
1031+ screen . getByText ( / p r o t e c t e d c a n d i d a t e v 0 \. 6 \. 0 i s s t a g e d / i) ,
1032+ ) . toBeInTheDocument ( ) ;
1033+ expect (
1034+ screen . queryByText ( / r e m a i n u n a v a i l a b l e i n t h e p u b l i c i n s t a l l e r / i) ,
1035+ ) . toBeNull ( ) ;
1036+ expect (
1037+ screen . getByText (
1038+ / a l l f i v e f i r m w a r e t a r g e t s , i n c l u d i n g E S P 3 2 - C 3 a n d R a s p b e r r y P i P i c o 2 W / i,
1039+ ) ,
1040+ ) . toHaveTextContent ( / p e n d i n g h a r d w a r e v a l i d a t i o n / i) ;
1041+
1042+ // The availability grid presents all five targets consistently with
1043+ // the candidate deployment mode.
1044+ for ( const id of fiveProfileOrder ) {
1045+ const card = screen . getByText ( id ) . closest ( "div" ) ;
1046+ expect ( card ) . toHaveTextContent (
1047+ "v0.6.0 protected candidate · HIL pending" ,
1048+ ) ;
1049+ expect ( card ) . not . toHaveTextContent ( / p l a n n e d / i) ;
1050+ }
1051+ expect ( screen . getByText ( "rpi-pico2-w" ) . closest ( "div" ) ) . toHaveTextContent (
1052+ "Raspberry Pi Pico 2 W" ,
1053+ ) ;
1054+ expect ( screen . getByText ( "esp32-c3-4mb" ) . closest ( "div" ) ) . toHaveTextContent (
1055+ "ESP32-C3" ,
1056+ ) ;
1057+
1058+ // The real-hardware board photo stays visible while the candidate
1059+ // offers the exact-board profile.
1060+ const boardPhoto = screen . getByRole ( "img" , {
1061+ name : "Actual Waveshare ESP32-S3-LCD-1.47B displaying the PyBLE v0.5.0 boot splash and app QR" ,
1062+ } ) ;
1063+ expect ( boardPhoto . closest ( "figure" ) ) . toHaveTextContent (
1064+ / A c t u a l b o a r d .* P y B L E f i r m w a r e v 0 \. 5 \. 0 / i,
1065+ ) ;
1066+
1067+ home . unmount ( ) ;
1068+ render ( < SupportPage /> ) ;
9451069 expect (
946- screen . getByText ( / f i r m w a r e i n s t a l l e r i s c u r r e n t l y u n a v a i l a b l e / i) ,
1070+ screen . getByText ( / p r o t e c t e d c a n d i d a t e v 0 \. 6 \. 0 i s s t a g e d / i) ,
9471071 ) . toBeInTheDocument ( ) ;
1072+ expect (
1073+ screen . queryByText ( / f i r m w a r e i n s t a l l e r i s c u r r e n t l y u n a v a i l a b l e / i) ,
1074+ ) . toBeNull ( ) ;
1075+ expect (
1076+ screen . queryByText ( / E S P 3 2 - C 3 i s n o t c u r r e n t l y a v a i l a b l e / i) ,
1077+ ) . toBeNull ( ) ;
9481078 } finally {
9491079 if ( previousSelection === undefined ) {
9501080 delete process . env . PYBLE_FLASH_SELECTION_FILE ;
@@ -977,6 +1107,20 @@ describe("public-site contract", () => {
9771107 ) . toBeInTheDocument ( ) ;
9781108 flash . unmount ( ) ;
9791109
1110+ const home = render ( < HomePage /> ) ;
1111+ expect (
1112+ screen . getByText ( / q u a l i f i e d p u b l i c v 0 \. 6 \. 0 f i r m w a r e / i) ,
1113+ ) . toHaveTextContent ( / a l l f i v e e x a c t r e l e a s e p r o f i l e s / i) ;
1114+ expect (
1115+ screen . queryByText ( / r e m a i n u n a v a i l a b l e i n t h e p u b l i c i n s t a l l e r / i) ,
1116+ ) . toBeNull ( ) ;
1117+ for ( const id of fiveProfileOrder ) {
1118+ expect ( screen . getByText ( id ) . closest ( "div" ) ) . toHaveTextContent (
1119+ "v0.6.0 qualified public release" ,
1120+ ) ;
1121+ }
1122+ home . unmount ( ) ;
1123+
9801124 render ( < SupportPage /> ) ;
9811125 const qualifiedCopy = screen . getByText (
9821126 / q u a l i f i e d v 0 \. 6 \. 0 f i r m w a r e i s a v a i l a b l e / i,
0 commit comments