@@ -9,6 +9,7 @@ import type { RenderResult } from '@testing-library/react-native';
99import { defaultFlexTokens } from '@fluentui-react-native/design/testing' ;
1010
1111import { Checkbox } from './checkbox' ;
12+ import type { CheckboxStatus } from './checkbox.types' ;
1213
1314function renderCheckbox ( props : React . ComponentProps < typeof Checkbox > ) : Promise < RenderResult > {
1415 return render ( < Checkbox { ...props } /> ) ;
@@ -330,4 +331,214 @@ describe('Checkbox', () => {
330331 } ) ;
331332 expect ( StyleSheet . flatten ( component . getByTestId ( 'checkbox-label' ) . props . style ) . color ) . toBe ( tokens . foregroundNeutralDisabled ) ;
332333 } ) ;
334+
335+ describe ( 'SPEC regression coverage' , ( ) => {
336+ afterEach ( ( ) => jest . restoreAllMocks ( ) ) ;
337+
338+ it ( 'CBX-001 resolves omitted defaults rather than caller-supplied values' , async ( ) => {
339+ const component = await renderCheckbox ( { } ) ;
340+ expect ( getRoot ( component ) . props ) . toMatchObject ( {
341+ accessible : true ,
342+ accessibilityLabel : 'Label' ,
343+ accessibilityState : { checked : false , disabled : false } ,
344+ focusable : true ,
345+ } ) ;
346+ expect ( getRoot ( component ) ) . toBeEnabled ( ) ;
347+ expect ( component . getByText ( 'Label' ) ) . toBeOnTheScreen ( ) ;
348+ expect ( component . queryByTestId ( 'checkbox-secondary' , { includeHiddenElements : true } ) ) . toBeNull ( ) ;
349+ expect ( component . queryByTestId ( 'checkbox-check-icon' ) ) . toBeNull ( ) ;
350+ expect ( component . queryByTestId ( 'checkbox-dash-icon' ) ) . toBeNull ( ) ;
351+ expect ( getIndicatorStyle ( component ) ) . toMatchObject ( {
352+ borderRadius : defaultFlexTokens . borderRadius . base100 ,
353+ height : 16 ,
354+ width : 16 ,
355+ } ) ;
356+ } ) ;
357+
358+ it ( 'CBX-001 forwards native root props and the original layout event' , async ( ) => {
359+ const onLayout = jest . fn ( ) ;
360+ const hitSlop = { bottom : 4 , left : 8 , right : 8 , top : 4 } ;
361+ const component = await renderCheckbox ( { focusable : false , hitSlop, nativeID : 'choice' , onLayout, testID : 'consumer-checkbox' } ) ;
362+ const root = getRoot ( component ) ;
363+ expect ( root ) . toBe ( component . getByTestId ( 'consumer-checkbox' ) ) ;
364+ expect ( root . props ) . toMatchObject ( { focusable : false , hitSlop, nativeID : 'choice' } ) ;
365+ const event = { nativeEvent : { layout : { height : 24 , width : 120 , x : 0 , y : 0 } } } ;
366+ await fireEvent ( root , 'layout' , event ) ;
367+ expect ( onLayout ) . toHaveBeenCalledTimes ( 1 ) ;
368+ expect ( onLayout ) . toHaveBeenCalledWith ( event ) ;
369+ } ) ;
370+
371+ describe . each ( [ 'unchecked' , 'checked' , 'indeterminate' ] as const ) ( 'CBX-002 starting from %s' , ( status ) => {
372+ it . each ( [ false , true ] ) ( 'preserves status ownership and callback order with controlled=%s' , async ( controlled ) => {
373+ const calls : string [ ] = [ ] ;
374+ const onStatusChange = jest . fn ( ( next : CheckboxStatus ) => calls . push ( next ) ) ;
375+ const onPress = jest . fn ( ( ) => calls . push ( 'press' ) ) ;
376+ const component = await renderCheckbox ( {
377+ defaultStatus : controlled ? ( status === 'checked' ? 'unchecked' : 'checked' ) : status ,
378+ onPress,
379+ onStatusChange,
380+ ...( controlled ? { status } : { } ) ,
381+ } ) ;
382+ const initialChecked = status === 'indeterminate' ? 'mixed' : status === 'checked' ;
383+ expect ( getRoot ( component ) . props . accessibilityState . checked ) . toBe ( initialChecked ) ;
384+ const first = status === 'checked' ? 'unchecked' : 'checked' ;
385+ const second = controlled ? first : first === 'checked' ? 'unchecked' : 'checked' ;
386+ const event = { nativeEvent : { pageX : 12 , pageY : 12 } } ;
387+
388+ await fireEvent . press ( getRoot ( component ) , event ) ;
389+ expect ( getRoot ( component ) . props . accessibilityState . checked ) . toBe ( controlled ? initialChecked : first === 'checked' ) ;
390+ await fireEvent . press ( getRoot ( component ) , event ) ;
391+ expect ( getRoot ( component ) . props . accessibilityState . checked ) . toBe ( controlled ? initialChecked : second === 'checked' ) ;
392+ expect ( onStatusChange . mock . calls ) . toEqual ( [ [ first ] , [ second ] ] ) ;
393+ expect ( onPress ) . toHaveBeenCalledTimes ( 2 ) ;
394+ for ( const call of [ 1 , 2 ] ) {
395+ expect ( onPress ) . toHaveBeenNthCalledWith (
396+ call ,
397+ expect . objectContaining ( { nativeEvent : expect . objectContaining ( event . nativeEvent ) } ) ,
398+ ) ;
399+ }
400+ expect ( calls ) . toEqual ( [ first , 'press' , second , 'press' ] ) ;
401+ } ) ;
402+
403+ it ( 'does not report or apply a disabled press' , async ( ) => {
404+ const onPress = jest . fn ( ) ;
405+ const onStatusChange = jest . fn ( ) ;
406+ const component = await renderCheckbox ( { defaultStatus : status , disabled : true , onPress, onStatusChange } ) ;
407+ await fireEvent . press ( getRoot ( component ) ) ;
408+ expect ( onPress ) . not . toHaveBeenCalled ( ) ;
409+ expect ( onStatusChange ) . not . toHaveBeenCalled ( ) ;
410+ expect ( getRoot ( component ) . props . accessibilityState . checked ) . toBe ( status === 'indeterminate' ? 'mixed' : status === 'checked' ) ;
411+ } ) ;
412+ } ) ;
413+
414+ it ( 'CBX-002 keeps defaultStatus as an initializer and accepts later controlled values' , async ( ) => {
415+ const component = await renderCheckbox ( { defaultStatus : 'checked' } ) ;
416+ await component . rerender ( < Checkbox defaultStatus = "unchecked" /> ) ;
417+ expect ( getRoot ( component ) . props . accessibilityState . checked ) . toBe ( true ) ;
418+
419+ const controlled = await renderCheckbox ( { status : 'unchecked' , defaultStatus : 'checked' } ) ;
420+ await controlled . rerender ( < Checkbox status = "indeterminate" defaultStatus = "checked" /> ) ;
421+ expect ( getRoot ( controlled ) . props . accessibilityState . checked ) . toBe ( 'mixed' ) ;
422+ } ) ;
423+
424+ it . each ( [
425+ [ false , false ] ,
426+ [ false , true ] ,
427+ [ true , false ] ,
428+ [ true , true ] ,
429+ ] ) ( 'CBX-003 gates text and development warnings with showLabel=%s and showSecondaryText=%s' , async ( showLabel , showSecondaryText ) => {
430+ const warn = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
431+ const component = await renderCheckbox ( { showLabel, showSecondaryText, accessibilityHint : 'Caller hint' } ) ;
432+ expect ( component . queryByTestId ( 'checkbox-label' ) !== null ) . toBe ( showLabel ) ;
433+ expect ( component . queryByTestId ( 'checkbox-secondary' , { includeHiddenElements : true } ) !== null ) . toBe ( showLabel && showSecondaryText ) ;
434+ expect ( getRoot ( component ) . props . accessibilityHint ) . toBe ( showLabel && showSecondaryText ? 'Caller hint. Description' : 'Caller hint' ) ;
435+ if ( showSecondaryText && ! showLabel ) {
436+ expect ( warn . mock . calls ) . toEqual ( [ [ 'Checkbox: secondary text requires a visible label.' ] ] ) ;
437+ } else {
438+ expect ( warn ) . not . toHaveBeenCalled ( ) ;
439+ }
440+ } ) ;
441+
442+ it ( 'CBX-003 and CBX-004 render indicator, label, and secondary text in order without duplicate accessible nodes' , async ( ) => {
443+ const component = await renderCheckbox ( { showSecondaryText : true } ) ;
444+ const indicator = component . getByTestId ( 'checkbox-indicator' ) ;
445+ const label = component . getByTestId ( 'checkbox-label' ) ;
446+ const secondary = component . getByTestId ( 'checkbox-secondary' , { includeHiddenElements : true } ) ;
447+ expect ( getRoot ( component ) . children ) . toEqual ( [
448+ indicator ,
449+ expect . objectContaining ( {
450+ props : expect . objectContaining ( { accessible : false } ) ,
451+ children : [ label , secondary ] ,
452+ } ) ,
453+ ] ) ;
454+ expect ( indicator . props . accessible ) . toBe ( false ) ;
455+ expect ( label . props . accessible ) . toBe ( false ) ;
456+ expect ( secondary . props ) . toMatchObject ( { accessible : false , accessibilityElementsHidden : true } ) ;
457+ expect ( secondary ) . toHaveTextContent ( 'Description' ) ;
458+ } ) ;
459+
460+ it ( 'CBX-004 owns role and checked/disabled state while preserving caller context' , async ( ) => {
461+ const component = await renderCheckbox ( {
462+ accessibilityRole : 'button' ,
463+ accessibilityState : { busy : true , checked : false , disabled : true } ,
464+ accessibilityLabel : 'Explicit option name' ,
465+ accessibilityHint : 'Caller context' ,
466+ secondaryText : 'Supporting context' ,
467+ showSecondaryText : true ,
468+ status : 'indeterminate' ,
469+ } ) ;
470+ expect ( getRoot ( component ) . props ) . toMatchObject ( {
471+ accessibilityRole : 'checkbox' ,
472+ accessibilityState : { busy : true , checked : 'mixed' , disabled : false } ,
473+ accessibilityLabel : 'Explicit option name' ,
474+ accessibilityHint : 'Caller context. Supporting context' ,
475+ } ) ;
476+ } ) ;
477+
478+ it . each ( [ 'unchecked' , 'checked' , 'indeterminate' ] as const ) (
479+ 'CBX-005 resolves all %s colors and restores hover after release' ,
480+ async ( status ) => {
481+ const colors = defaultFlexTokens . color ;
482+ const component = await renderCheckbox ( { status, showSecondaryText : true } ) ;
483+ for ( const [ event , palette ] of [
484+ [ 'hoverIn' , colors . hover ] ,
485+ [ 'pressIn' , colors . pressed ] ,
486+ [ 'pressOut' , colors . hover ] ,
487+ [ 'hoverOut' , colors ] ,
488+ ] as const ) {
489+ await fireEvent ( getRoot ( component ) , event , { } ) ;
490+ expect ( getIndicatorStyle ( component ) ) . toMatchObject ( {
491+ backgroundColor : status === 'unchecked' ? palette . backgroundNeutralTransparent : palette . backgroundBrandHeavy ,
492+ borderColor : status === 'unchecked' ? palette . strokeNeutralHeavy : palette . strokeNeutralTransparent ,
493+ } ) ;
494+ expect ( StyleSheet . flatten ( component . getByTestId ( 'checkbox-label' ) . props . style ) . color ) . toBe (
495+ status === 'unchecked' ? palette . foregroundNeutralSecondary : palette . foregroundNeutralPrimary ,
496+ ) ;
497+ expect ( StyleSheet . flatten ( component . getByTestId ( 'checkbox-secondary' , { includeHiddenElements : true } ) . props . style ) . color ) . toBe (
498+ palette . foregroundNeutralSecondary ,
499+ ) ;
500+ if ( status !== 'unchecked' ) {
501+ expect ( StyleSheet . flatten ( component . getByText ( status === 'checked' ? '\u2713' : '\u2212' ) . props . style ) . color ) . toBe (
502+ palette . foregroundBrandOnloud ,
503+ ) ;
504+ }
505+ }
506+ } ,
507+ ) ;
508+
509+ it . each ( [ 'unchecked' , 'checked' , 'indeterminate' ] as const ) (
510+ 'CBX-005 selects disabled %s colors during an active press' ,
511+ async ( status ) => {
512+ const component = await renderCheckbox ( { status, showSecondaryText : true } ) ;
513+ await fireEvent ( getRoot ( component ) , 'hoverIn' , { } ) ;
514+ await fireEvent ( getRoot ( component ) , 'pressIn' , { } ) ;
515+ await component . rerender ( < Checkbox status = { status } showSecondaryText disabled /> ) ;
516+ const colors = defaultFlexTokens . color ;
517+ expect ( getIndicatorStyle ( component ) ) . toMatchObject ( {
518+ backgroundColor : status === 'unchecked' ? colors . backgroundNeutralTransparent : colors . backgroundNeutralHeavyDisabled ,
519+ borderColor : status === 'unchecked' ? colors . strokeNeutralDisabled : colors . strokeNeutralTransparent ,
520+ } ) ;
521+ for ( const id of [ 'checkbox-label' , 'checkbox-secondary' ] ) {
522+ expect ( StyleSheet . flatten ( component . getByTestId ( id , { includeHiddenElements : true } ) . props . style ) . color ) . toBe (
523+ colors . foregroundNeutralDisabled ,
524+ ) ;
525+ }
526+ if ( status !== 'unchecked' ) {
527+ expect ( StyleSheet . flatten ( component . getByText ( status === 'checked' ? '\u2713' : '\u2212' ) . props . style ) . color ) . toBe (
528+ colors . foregroundNeutralDisabled ,
529+ ) ;
530+ }
531+ } ,
532+ ) ;
533+
534+ it ( 'CBX-005 applies the last caller style after structural and token styles' , async ( ) => {
535+ const component = await renderCheckbox ( {
536+ style : [
537+ { minHeight : 40 , gap : 20 } ,
538+ { minHeight : 48 , gap : 12 , flexDirection : 'column' } ,
539+ ] ,
540+ } ) ;
541+ expect ( getRootStyle ( component ) ) . toMatchObject ( { minHeight : 48 , gap : 12 , flexDirection : 'column' } ) ;
542+ } ) ;
543+ } ) ;
333544} ) ;
0 commit comments