@@ -12,6 +12,7 @@ import {
1212 useGloss ,
1313 useGlossDispatch ,
1414 useMorphemeBreakdownDispatch ,
15+ useMorphemeDeleteDispatch ,
1516 useMorphemeGlossDispatch ,
1617 useMorphemes ,
1718 usePhraseLinkByIdMap ,
@@ -864,6 +865,22 @@ function MorphemeWriter({
864865 ) ;
865866}
866867
868+ /**
869+ * Renders a button that dispatches a morpheme breakdown deletion, used to test
870+ * `useMorphemeDeleteDispatch`.
871+ *
872+ * @param props.tokenRef - Token ref whose breakdown to delete.
873+ * @returns JSX element suitable for passing to `render`.
874+ */
875+ function MorphemeDeleter ( { tokenRef } : Readonly < { tokenRef : string } > ) {
876+ const dispatch = useMorphemeDeleteDispatch ( ) ;
877+ return (
878+ < button onClick = { ( ) => dispatch ( tokenRef ) } type = "button" >
879+ delete-morphemes
880+ </ button >
881+ ) ;
882+ }
883+
867884/**
868885 * Renders a button that dispatches a morpheme gloss, used to test `useMorphemeGlossDispatch`.
869886 *
@@ -925,6 +942,16 @@ function MorphemeGlossDispatchUser() {
925942 return undefined ;
926943}
927944
945+ /**
946+ * Renders a component that calls `useMorphemeDeleteDispatch` without a provider.
947+ *
948+ * @returns Nothing — only mounted to trigger the throw.
949+ */
950+ function MorphemeDeleteDispatchUser ( ) {
951+ useMorphemeDeleteDispatch ( ) ;
952+ return undefined ;
953+ }
954+
928955describe ( 'useMorphemes' , ( ) => {
929956 it ( 'returns empty array when no morphemes exist' , ( ) => {
930957 render (
@@ -1018,6 +1045,55 @@ describe('useMorphemeBreakdownDispatch', () => {
10181045 } ) ;
10191046} ) ;
10201047
1048+ describe ( 'useMorphemeDeleteDispatch' , ( ) => {
1049+ it ( 'removes the morpheme breakdown and calls onSave' , async ( ) => {
1050+ const onSave = jest . fn ( ) ;
1051+ const ta : TokenAnalysis = {
1052+ id : 'ta-1' ,
1053+ surfaceText : 'cat' ,
1054+ morphemes : [
1055+ { id : 'm-1' , form : 'ca' , writingSystem : 'und' } ,
1056+ { id : 'm-2' , form : '-t' , writingSystem : 'und' } ,
1057+ ] ,
1058+ } ;
1059+ const link : TokenAnalysisLink = {
1060+ analysisId : 'ta-1' ,
1061+ status : 'approved' ,
1062+ token : { tokenRef : 'tok-1' , surfaceText : 'cat' } ,
1063+ } ;
1064+ const analysis : TextAnalysis = {
1065+ segmentAnalyses : [ ] ,
1066+ segmentAnalysisLinks : [ ] ,
1067+ tokenAnalyses : [ ta ] ,
1068+ tokenAnalysisLinks : [ link ] ,
1069+ phraseAnalyses : [ ] ,
1070+ phraseAnalysisLinks : [ ] ,
1071+ } ;
1072+ render (
1073+ < AnalysisStoreProvider initialAnalysis = { analysis } analysisLanguage = "und" onSave = { onSave } >
1074+ < MorphemeDeleter tokenRef = "tok-1" />
1075+ < MorphemeReader tokenRef = "tok-1" />
1076+ </ AnalysisStoreProvider > ,
1077+ ) ;
1078+
1079+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'delete-morphemes' } ) ) ;
1080+
1081+ expect ( screen . getByTestId ( 'morphemes' ) ) . toHaveTextContent ( '' ) ;
1082+ expect ( onSave ) . toHaveBeenCalledTimes ( 1 ) ;
1083+ const saved : TextAnalysis = onSave . mock . calls [ 0 ] [ 0 ] ;
1084+ // The analysis carried no gloss, so the now-empty record and its link are removed entirely.
1085+ expect ( saved . tokenAnalyses ) . toHaveLength ( 0 ) ;
1086+ expect ( saved . tokenAnalysisLinks ) . toHaveLength ( 0 ) ;
1087+ } ) ;
1088+
1089+ it ( 'throws when called outside an AnalysisStoreProvider' , ( ) => {
1090+ jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
1091+ expect ( ( ) => render ( < MorphemeDeleteDispatchUser /> ) ) . toThrow (
1092+ 'useMorphemeDeleteDispatch must be used inside an AnalysisStoreProvider' ,
1093+ ) ;
1094+ } ) ;
1095+ } ) ;
1096+
10211097describe ( 'useMorphemeGlossDispatch' , ( ) => {
10221098 it ( 'writes a morpheme gloss and calls onSave' , async ( ) => {
10231099 const onSave = jest . fn ( ) ;
0 commit comments