@@ -13,6 +13,8 @@ import {
1313 usePhraseLinkForToken ,
1414 usePhraseLinkMap ,
1515 usePhraseDispatch ,
16+ usePhraseGloss ,
17+ usePhraseGlossDispatch ,
1618} from '../../components/AnalysisStore' ;
1719
1820// ---------------------------------------------------------------------------
@@ -591,3 +593,133 @@ describe('usePhraseDispatch', () => {
591593 ) ;
592594 } ) ;
593595} ) ;
596+
597+ // ---------------------------------------------------------------------------
598+ // usePhraseGloss
599+ // ---------------------------------------------------------------------------
600+
601+ /**
602+ * Renders the phrase gloss for a given phraseId, used to assert on `usePhraseGloss`.
603+ *
604+ * @param props - Component props.
605+ * @param props.phraseId - Phrase id to look up.
606+ * @returns JSX element.
607+ */
608+ function PhraseGlossReader ( { phraseId } : Readonly < { phraseId : string } > ) {
609+ const gloss = usePhraseGloss ( phraseId ) ;
610+ return < span data-testid = "phrase-gloss" > { gloss } </ span > ;
611+ }
612+
613+ /**
614+ * Renders a component that calls `usePhraseGloss` without a provider, to assert it throws.
615+ *
616+ * @returns Nothing — only mounted to trigger the throw.
617+ */
618+ function PhraseGlossUser ( ) {
619+ usePhraseGloss ( 'p1' ) ;
620+ return undefined ;
621+ }
622+
623+ /** A `TextAnalysis` with a phrase that has a gloss in the `'und'` language. */
624+ const PHRASE_ANALYSIS_WITH_GLOSS : TextAnalysis = {
625+ segmentAnalyses : [ ] ,
626+ segmentAnalysisLinks : [ ] ,
627+ tokenAnalyses : [ ] ,
628+ tokenAnalysisLinks : [ ] ,
629+ phraseAnalyses : [
630+ { id : 'phrase-1' , surfaceText : 'Hello World' , gloss : { und : 'world beginning' } } ,
631+ ] ,
632+ phraseAnalysisLinks : [
633+ {
634+ analysisId : 'phrase-1' ,
635+ status : 'approved' ,
636+ tokens : [ { tokenRef : 'tok-a' , surfaceText : 'Hello' } ] ,
637+ } ,
638+ ] ,
639+ } ;
640+
641+ describe ( 'usePhraseGloss' , ( ) => {
642+ it ( 'returns empty string when phraseId is not found' , ( ) => {
643+ render (
644+ < AnalysisStoreProvider analysisLanguage = "und" >
645+ < PhraseGlossReader phraseId = "missing" />
646+ </ AnalysisStoreProvider > ,
647+ ) ;
648+ expect ( screen . getByTestId ( 'phrase-gloss' ) ) . toHaveTextContent ( '' ) ;
649+ } ) ;
650+
651+ it ( 'returns the gloss for the active analysis language' , ( ) => {
652+ render (
653+ < AnalysisStoreProvider initialAnalysis = { PHRASE_ANALYSIS_WITH_GLOSS } analysisLanguage = "und" >
654+ < PhraseGlossReader phraseId = "phrase-1" />
655+ </ AnalysisStoreProvider > ,
656+ ) ;
657+ expect ( screen . getByTestId ( 'phrase-gloss' ) ) . toHaveTextContent ( 'world beginning' ) ;
658+ } ) ;
659+
660+ it ( 'throws when called outside an AnalysisStoreProvider' , ( ) => {
661+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
662+ expect ( ( ) => render ( < PhraseGlossUser /> ) ) . toThrow (
663+ 'usePhraseGloss must be used inside an AnalysisStoreProvider' ,
664+ ) ;
665+ } ) ;
666+ } ) ;
667+
668+ // ---------------------------------------------------------------------------
669+ // usePhraseGlossDispatch
670+ // ---------------------------------------------------------------------------
671+
672+ /**
673+ * Renders a button that writes a phrase gloss via `usePhraseGlossDispatch`.
674+ *
675+ * @param props - Component props.
676+ * @param props.phraseId - Phrase id to write.
677+ * @param props.value - Gloss value to write.
678+ * @returns JSX element.
679+ */
680+ function PhraseGlossWriter ( { phraseId, value } : Readonly < { phraseId : string ; value : string } > ) {
681+ const dispatch = usePhraseGlossDispatch ( ) ;
682+ return (
683+ < button onClick = { ( ) => dispatch ( phraseId , value ) } type = "button" >
684+ write
685+ </ button >
686+ ) ;
687+ }
688+
689+ /**
690+ * Renders a component that calls `usePhraseGlossDispatch` without a provider, to assert it throws.
691+ *
692+ * @returns Nothing — only mounted to trigger the throw.
693+ */
694+ function PhraseGlossDispatchUser ( ) {
695+ usePhraseGlossDispatch ( ) ;
696+ return undefined ;
697+ }
698+
699+ describe ( 'usePhraseGlossDispatch' , ( ) => {
700+ it ( 'writes the phrase gloss and triggers onSave' , async ( ) => {
701+ const onSave = jest . fn ( ) ;
702+ render (
703+ < AnalysisStoreProvider
704+ initialAnalysis = { PHRASE_ANALYSIS }
705+ analysisLanguage = "und"
706+ onSave = { onSave }
707+ >
708+ < PhraseGlossWriter phraseId = "phrase-1" value = "beginning" />
709+ </ AnalysisStoreProvider > ,
710+ ) ;
711+
712+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'write' } ) ) ;
713+
714+ expect ( onSave ) . toHaveBeenCalledTimes ( 1 ) ;
715+ const saved : TextAnalysis = onSave . mock . calls [ 0 ] [ 0 ] ;
716+ expect ( saved . phraseAnalyses [ 0 ] . gloss ) . toStrictEqual ( { und : 'beginning' } ) ;
717+ } ) ;
718+
719+ it ( 'throws when called outside an AnalysisStoreProvider' , ( ) => {
720+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
721+ expect ( ( ) => render ( < PhraseGlossDispatchUser /> ) ) . toThrow (
722+ 'usePhraseGlossDispatch must be used inside an AnalysisStoreProvider' ,
723+ ) ;
724+ } ) ;
725+ } ) ;
0 commit comments