@@ -22,6 +22,8 @@ import {
2222 usePhraseGloss ,
2323 usePhraseGlossDispatch ,
2424 useReportGlossEditing ,
25+ useSegmentFreeTranslation ,
26+ useSegmentFreeTranslationDispatch ,
2527} from '../../components/AnalysisStore' ;
2628
2729// ---------------------------------------------------------------------------
@@ -819,6 +821,144 @@ describe('usePhraseGlossDispatch', () => {
819821 } ) ;
820822} ) ;
821823
824+ // ---------------------------------------------------------------------------
825+ // useSegmentFreeTranslation
826+ // ---------------------------------------------------------------------------
827+
828+ /** A `TextAnalysis` with an approved segment analysis carrying a free translation in `'und'`. */
829+ const SEGMENT_ANALYSIS_WITH_TRANSLATION : TextAnalysis = {
830+ segmentAnalyses : [
831+ { id : 'sa-1' , surfaceText : 'In the beginning' , freeTranslation : { und : 'au commencement' } } ,
832+ ] ,
833+ segmentAnalysisLinks : [ { analysisId : 'sa-1' , status : 'approved' , segmentId : 'seg-1' } ] ,
834+ tokenAnalyses : [ ] ,
835+ tokenAnalysisLinks : [ ] ,
836+ phraseAnalyses : [ ] ,
837+ phraseAnalysisLinks : [ ] ,
838+ } ;
839+
840+ /**
841+ * Renders the free translation for a given segmentId, used to assert on
842+ * `useSegmentFreeTranslation`.
843+ *
844+ * @param props - Component props.
845+ * @param props.segmentId - Segment id to look up.
846+ * @returns JSX element.
847+ */
848+ function SegmentTranslationReader ( { segmentId } : Readonly < { segmentId : string } > ) {
849+ const value = useSegmentFreeTranslation ( segmentId ) ;
850+ return < span data-testid = "segment-translation" > { value } </ span > ;
851+ }
852+
853+ /**
854+ * Renders a component that calls `useSegmentFreeTranslation` without a provider, to assert it
855+ * throws.
856+ *
857+ * @returns Nothing — only mounted to trigger the throw.
858+ */
859+ function SegmentTranslationUser ( ) {
860+ useSegmentFreeTranslation ( 'seg-1' ) ;
861+ return undefined ;
862+ }
863+
864+ describe ( 'useSegmentFreeTranslation' , ( ) => {
865+ it ( 'returns empty string when the segment has no approved analysis' , ( ) => {
866+ render (
867+ < AnalysisStoreProvider analysisLanguage = "und" >
868+ < SegmentTranslationReader segmentId = "seg-1" />
869+ </ AnalysisStoreProvider > ,
870+ ) ;
871+ expect ( screen . getByTestId ( 'segment-translation' ) ) . toHaveTextContent ( '' ) ;
872+ } ) ;
873+
874+ it ( 'returns the free translation for the active analysis language' , ( ) => {
875+ render (
876+ < AnalysisStoreProvider
877+ initialAnalysis = { SEGMENT_ANALYSIS_WITH_TRANSLATION }
878+ analysisLanguage = "und"
879+ >
880+ < SegmentTranslationReader segmentId = "seg-1" />
881+ </ AnalysisStoreProvider > ,
882+ ) ;
883+ expect ( screen . getByTestId ( 'segment-translation' ) ) . toHaveTextContent ( 'au commencement' ) ;
884+ } ) ;
885+
886+ it ( 'throws when called outside an AnalysisStoreProvider' , ( ) => {
887+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
888+ expect ( ( ) => render ( < SegmentTranslationUser /> ) ) . toThrow (
889+ 'useSegmentFreeTranslation must be used inside an AnalysisStoreProvider' ,
890+ ) ;
891+ } ) ;
892+ } ) ;
893+
894+ // ---------------------------------------------------------------------------
895+ // useSegmentFreeTranslationDispatch
896+ // ---------------------------------------------------------------------------
897+
898+ /**
899+ * Renders a button that writes a segment free translation via `useSegmentFreeTranslationDispatch`.
900+ *
901+ * @param props - Component props.
902+ * @param props.segmentId - Segment id to write.
903+ * @param props.surfaceText - Segment baseline text to store.
904+ * @param props.value - Free-translation value to write.
905+ * @returns JSX element.
906+ */
907+ function SegmentTranslationWriter ( {
908+ segmentId,
909+ surfaceText,
910+ value,
911+ } : Readonly < { segmentId : string ; surfaceText : string ; value : string } > ) {
912+ const dispatch = useSegmentFreeTranslationDispatch ( ) ;
913+ return (
914+ < button onClick = { ( ) => dispatch ( segmentId , surfaceText , value ) } type = "button" >
915+ write
916+ </ button >
917+ ) ;
918+ }
919+
920+ /**
921+ * Renders a component that calls `useSegmentFreeTranslationDispatch` without a provider, to assert
922+ * it throws.
923+ *
924+ * @returns Nothing — only mounted to trigger the throw.
925+ */
926+ function SegmentTranslationDispatchUser ( ) {
927+ useSegmentFreeTranslationDispatch ( ) ;
928+ return undefined ;
929+ }
930+
931+ describe ( 'useSegmentFreeTranslationDispatch' , ( ) => {
932+ it ( 'writes the segment free translation and triggers onSave' , async ( ) => {
933+ const onSave = jest . fn ( ) ;
934+ render (
935+ < AnalysisStoreProvider analysisLanguage = "und" onSave = { onSave } >
936+ < SegmentTranslationWriter
937+ segmentId = "seg-1"
938+ surfaceText = "In the beginning"
939+ value = "au commencement"
940+ />
941+ </ AnalysisStoreProvider > ,
942+ ) ;
943+
944+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'write' } ) ) ;
945+
946+ expect ( onSave ) . toHaveBeenCalledTimes ( 1 ) ;
947+ const saved : TextAnalysis = onSave . mock . calls [ 0 ] [ 0 ] ;
948+ expect ( saved . segmentAnalyses [ 0 ] ) . toMatchObject ( {
949+ surfaceText : 'In the beginning' ,
950+ freeTranslation : { und : 'au commencement' } ,
951+ } ) ;
952+ } ) ;
953+
954+ it ( 'throws when called outside an AnalysisStoreProvider' , ( ) => {
955+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
956+ expect ( ( ) => render ( < SegmentTranslationDispatchUser /> ) ) . toThrow (
957+ 'useSegmentFreeTranslationDispatch must be used inside an AnalysisStoreProvider' ,
958+ ) ;
959+ } ) ;
960+ } ) ;
961+
822962// ---------------------------------------------------------------------------
823963// Morpheme hooks
824964// ---------------------------------------------------------------------------
0 commit comments