1- import { DocumentDefinition , DocumentDefinitionType , DocumentType } from '../../models/datamapper' ;
1+ import { DocumentDefinition , DocumentDefinitionType , DocumentType , IField } from '../../models/datamapper' ;
22import { NS_XML_SCHEMA } from '../../models/datamapper/standard-namespaces' ;
3+ import { FieldOverrideVariant } from '../../models/datamapper/types' ;
4+ import { getChoiceWithAbstractXsd } from '../../stubs/datamapper/data-mapper' ;
35import { XmlSchemaCollection } from '../../xml-schema-ts' ;
46import { SchemaPathService } from '../schema-path.service' ;
57import { ChoiceSelectionService } from './choice-selection.service' ;
8+ import { FieldOverrideService } from './field-override.service' ;
69import { XmlSchemaDocument , XmlSchemaField } from './xml-schema/xml-schema-document.model' ;
10+ import { XmlSchemaDocumentService } from './xml-schema/xml-schema-document.service' ;
11+
12+ const NS_CHOICE_ABSTRACT = 'http://www.example.com/CHOICE_ABSTRACT' ;
13+
14+ function createChoiceWithAbstractDoc ( ) {
15+ const definition = new DocumentDefinition ( DocumentType . SOURCE_BODY , DocumentDefinitionType . XML_SCHEMA , 'test-doc' , {
16+ 'ChoiceWithAbstract.xsd' : getChoiceWithAbstractXsd ( ) ,
17+ } ) ;
18+ definition . rootElementChoice = { namespaceUri : NS_CHOICE_ABSTRACT , name : 'Notification' } ;
19+ const result = XmlSchemaDocumentService . createXmlSchemaDocument ( definition ) ;
20+ if ( result . validationStatus !== 'success' || ! result . document ) {
21+ throw new Error (
22+ result . errors ?. map ( ( e ) => e . message ) . join ( '; ' ) || 'Failed to create choice+abstract test document' ,
23+ ) ;
24+ }
25+ return result . document ;
26+ }
27+
28+ function findChoiceField ( doc : XmlSchemaDocument ) : XmlSchemaField {
29+ const root = doc . fields [ 0 ] ;
30+ const choice = root . fields . find ( ( f ) => f . wrapperKind === 'choice' ) ;
31+ if ( ! choice ) throw new Error ( 'Choice field not found in Notification' ) ;
32+ return choice ;
33+ }
34+
35+ function findAbstractField ( choiceField : IField ) : XmlSchemaField {
36+ const abstract = choiceField . fields . find ( ( f ) => f . wrapperKind === 'abstract' ) ;
37+ if ( ! abstract ) throw new Error ( 'Abstract field not found inside choice' ) ;
38+ return abstract as XmlSchemaField ;
39+ }
40+
41+ function findDescendantLeaf ( field : IField ) : IField {
42+ let current = field ;
43+ while ( current . fields . length > 0 ) {
44+ current = current . fields [ 0 ] ;
45+ }
46+ return current ;
47+ }
748
849describe ( 'ChoiceSelectionService' , ( ) => {
950 let document : XmlSchemaDocument ;
@@ -145,7 +186,7 @@ describe('ChoiceSelectionService', () => {
145186 expect ( document . definition . choiceSelections ?. length ) . toBe ( 2 ) ;
146187 } ) ;
147188
148- it ( 'should cascade invalidate true descendant selections' , ( ) => {
189+ it ( 'should preserve descendant choice selections' , ( ) => {
149190 const parentChoice = document . fields [ 0 ] . fields [ 0 ] ;
150191 const emailMember = parentChoice . fields [ 0 ] ;
151192 const nestedChoice = emailMember . fields [ 0 ] ;
@@ -155,9 +196,9 @@ describe('ChoiceSelectionService', () => {
155196
156197 ChoiceSelectionService . setChoiceSelection ( document , parentChoice , 1 , namespaceMap ) ;
157198
158- expect ( document . definition . choiceSelections ?. length ) . toBe ( 1 ) ;
159- expect ( document . definition . choiceSelections ?. [ 0 ] . schemaPath ) . not . toContain ( 'email' ) ;
160- expect ( nestedChoice . selectedMemberIndex ) . toBeUndefined ( ) ;
199+ // Both parent and nested selections preserved
200+ expect ( document . definition . choiceSelections ?. length ) . toBe ( 2 ) ;
201+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 0 ) ;
161202 } ) ;
162203 } ) ;
163204
@@ -214,7 +255,7 @@ describe('ChoiceSelectionService', () => {
214255 expect ( document . definition . choiceSelections ?. [ 0 ] . schemaPath ) . toContain ( 'Container' ) ;
215256 } ) ;
216257
217- it ( 'should cascade invalidate true descendant selections on clear' , ( ) => {
258+ it ( 'should preserve descendant choice selections on clear' , ( ) => {
218259 const parentChoice = document . fields [ 0 ] . fields [ 0 ] ;
219260 const emailMember = parentChoice . fields [ 0 ] ;
220261 const nestedChoice = emailMember . fields [ 0 ] ;
@@ -225,8 +266,285 @@ describe('ChoiceSelectionService', () => {
225266
226267 ChoiceSelectionService . clearChoiceSelection ( document , parentChoice , namespaceMap ) ;
227268
228- expect ( document . definition . choiceSelections ?. length ) . toBe ( 0 ) ;
229- expect ( nestedChoice . selectedMemberIndex ) . toBeUndefined ( ) ;
269+ // Nested selection preserved
270+ expect ( document . definition . choiceSelections ?. length ) . toBe ( 1 ) ;
271+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 1 ) ;
272+ } ) ;
273+ } ) ;
274+
275+ describe ( 'choice + descendant override interaction (issue #3234)' , ( ) => {
276+ describe ( 'abstract substitution preserved across choice changes' , ( ) => {
277+ it ( 'should preserve descendant abstract substitution when setting choice selection' , ( ) => {
278+ const doc = createChoiceWithAbstractDoc ( ) ;
279+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
280+ const choiceField = findChoiceField ( doc ) ;
281+ const abstractField = findAbstractField ( choiceField ) ;
282+
283+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
284+ const emailIndex = abstractField . selectedMemberIndex ;
285+ expect ( emailIndex ) . toBeDefined ( ) ;
286+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
287+
288+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
289+
290+ // Substitution stays in both definition and live field — consistent state
291+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
292+ expect ( abstractField . selectedMemberIndex ) . toBe ( emailIndex ) ;
293+ } ) ;
294+
295+ it ( 'should preserve descendant abstract substitution when clearing choice selection' , ( ) => {
296+ const doc = createChoiceWithAbstractDoc ( ) ;
297+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
298+ const choiceField = findChoiceField ( doc ) ;
299+ const abstractField = findAbstractField ( choiceField ) ;
300+
301+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
302+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
303+ const smsIndex = abstractField . selectedMemberIndex ;
304+ expect ( smsIndex ) . toBeDefined ( ) ;
305+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
306+
307+ ChoiceSelectionService . clearChoiceSelection ( doc , choiceField , nsMap ) ;
308+
309+ // Substitution preserved
310+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
311+ expect ( abstractField . selectedMemberIndex ) . toBe ( smsIndex ) ;
312+ } ) ;
313+
314+ it ( 'should allow reverting abstract substitution after parent choice selection (issue #3234)' , ( ) => {
315+ const doc = createChoiceWithAbstractDoc ( ) ;
316+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
317+ const choiceField = findChoiceField ( doc ) ;
318+ const abstractField = findAbstractField ( choiceField ) ;
319+
320+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
321+ expect ( abstractField . selectedMemberIndex ) . toBeDefined ( ) ;
322+
323+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
324+
325+ // Substitution is preserved, so revert should work
326+ FieldOverrideService . revertFieldSubstitution ( abstractField , nsMap ) ;
327+
328+ expect ( abstractField . selectedMemberIndex ) . toBeUndefined ( ) ;
329+ expect ( doc . definition . fieldSubstitutions ?? [ ] ) . toHaveLength ( 0 ) ;
330+ } ) ;
331+
332+ it ( 'should allow switching substitution after choice selection' , ( ) => {
333+ const doc = createChoiceWithAbstractDoc ( ) ;
334+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
335+ const choiceField = findChoiceField ( doc ) ;
336+ const abstractField = findAbstractField ( choiceField ) ;
337+
338+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
339+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
340+
341+ // Switch from Email to SMS
342+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
343+
344+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
345+ expect ( doc . definition . fieldSubstitutions ! [ 0 ] . name ) . toBe ( 'ca:SMS' ) ;
346+ const smsIndex = abstractField . fields . findIndex ( ( f ) => f . name === 'SMS' ) ;
347+ expect ( abstractField . selectedMemberIndex ) . toBe ( smsIndex ) ;
348+ } ) ;
349+
350+ it ( 'should allow revert after switching substitution across choice changes' , ( ) => {
351+ const doc = createChoiceWithAbstractDoc ( ) ;
352+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
353+ const choiceField = findChoiceField ( doc ) ;
354+ const abstractField = findAbstractField ( choiceField ) ;
355+
356+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
357+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
358+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
359+
360+ FieldOverrideService . revertFieldSubstitution ( abstractField , nsMap ) ;
361+
362+ expect ( abstractField . selectedMemberIndex ) . toBeUndefined ( ) ;
363+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 0 ) ;
364+ } ) ;
365+ } ) ;
366+
367+ describe ( 'type override preserved across choice changes' , ( ) => {
368+ it ( 'should preserve descendant type override when setting choice selection' , ( ) => {
369+ const doc = createChoiceWithAbstractDoc ( ) ;
370+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
371+ const choiceField = findChoiceField ( doc ) ;
372+
373+ const firstMember = choiceField . fields [ 0 ] ;
374+ const leafField = findDescendantLeaf ( firstMember ) ;
375+
376+ const leafPath = SchemaPathService . build ( leafField , nsMap ) ;
377+ leafField . typeOverride = FieldOverrideVariant . SAFE ;
378+ leafField . originalField = {
379+ name : leafField . name ,
380+ displayName : leafField . name ,
381+ namespaceURI : '' ,
382+ namespacePrefix : null ,
383+ type : leafField . type ,
384+ typeQName : null ,
385+ namedTypeFragmentRefs : [ ] ,
386+ } ;
387+ doc . definition . fieldTypeOverrides = [
388+ { schemaPath : leafPath , type : 'xs:int' , originalType : 'xs:string' , variant : FieldOverrideVariant . SAFE } ,
389+ ] ;
390+
391+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
392+
393+ expect ( doc . definition . fieldTypeOverrides ) . toHaveLength ( 1 ) ;
394+ expect ( leafField . typeOverride ) . toBe ( FieldOverrideVariant . SAFE ) ;
395+ } ) ;
396+
397+ it ( 'should preserve descendant type override when clearing choice selection' , ( ) => {
398+ const doc = createChoiceWithAbstractDoc ( ) ;
399+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
400+ const choiceField = findChoiceField ( doc ) ;
401+
402+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
403+
404+ const firstMember = choiceField . fields [ 0 ] ;
405+ const leafField = findDescendantLeaf ( firstMember ) ;
406+
407+ const leafPath = SchemaPathService . build ( leafField , nsMap ) ;
408+ leafField . typeOverride = FieldOverrideVariant . SAFE ;
409+ leafField . originalField = {
410+ name : leafField . name ,
411+ displayName : leafField . name ,
412+ namespaceURI : '' ,
413+ namespacePrefix : null ,
414+ type : leafField . type ,
415+ typeQName : null ,
416+ namedTypeFragmentRefs : [ ] ,
417+ } ;
418+ doc . definition . fieldTypeOverrides = [
419+ { schemaPath : leafPath , type : 'xs:int' , originalType : 'xs:string' , variant : FieldOverrideVariant . SAFE } ,
420+ ] ;
421+
422+ ChoiceSelectionService . clearChoiceSelection ( doc , choiceField , nsMap ) ;
423+
424+ expect ( doc . definition . fieldTypeOverrides ) . toHaveLength ( 1 ) ;
425+ expect ( leafField . typeOverride ) . toBe ( FieldOverrideVariant . SAFE ) ;
426+ } ) ;
427+ } ) ;
428+
429+ describe ( 'round-trip — overrides survive choice switch and switch-back (issue #3232)' , ( ) => {
430+ it ( 'should preserve nested choice selection across parent choice round-trip' , ( ) => {
431+ const doc = createTestDocumentWithChoices ( ) ;
432+ const nsMap = { xs : NS_XML_SCHEMA , ns0 : 'io.kaoto.test' } ;
433+ const parentChoice = doc . fields [ 0 ] . fields [ 0 ] ; // [email, phone, fax]
434+ const emailMember = parentChoice . fields [ 0 ] ;
435+ const nestedChoice = emailMember . fields [ 0 ] ; // [work, personal]
436+
437+ // Select nested choice inside email
438+ ChoiceSelectionService . setChoiceSelection ( doc , parentChoice , 0 , nsMap ) ;
439+ ChoiceSelectionService . setChoiceSelection ( doc , nestedChoice , 1 , nsMap ) ;
440+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 1 ) ;
441+
442+ // Switch parent away from email
443+ ChoiceSelectionService . setChoiceSelection ( doc , parentChoice , 2 , nsMap ) ;
444+
445+ // Switch parent back to email
446+ ChoiceSelectionService . setChoiceSelection ( doc , parentChoice , 0 , nsMap ) ;
447+
448+ // Nested selection survived the round-trip
449+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 1 ) ;
450+ expect ( doc . definition . choiceSelections ?. length ) . toBe ( 2 ) ;
451+ } ) ;
452+
453+ it ( 'should preserve substitution across parent choice round-trip' , ( ) => {
454+ const doc = createChoiceWithAbstractDoc ( ) ;
455+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
456+ const choiceField = findChoiceField ( doc ) ;
457+ const abstractField = findAbstractField ( choiceField ) ;
458+
459+ // Select substitution
460+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
461+ const emailIndex = abstractField . selectedMemberIndex ;
462+
463+ // Switch choice away and back
464+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 1 , nsMap ) ;
465+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
466+
467+ // Substitution survived
468+ expect ( abstractField . selectedMemberIndex ) . toBe ( emailIndex ) ;
469+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
470+ expect ( doc . definition . fieldSubstitutions ! [ 0 ] . name ) . toBe ( 'ca:Email' ) ;
471+ } ) ;
472+
473+ it ( 'should preserve substitution through multiple choice changes' , ( ) => {
474+ const doc = createChoiceWithAbstractDoc ( ) ;
475+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
476+ const choiceField = findChoiceField ( doc ) ;
477+ const abstractField = findAbstractField ( choiceField ) ;
478+
479+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
480+ const smsIndex = abstractField . selectedMemberIndex ;
481+
482+ // Multiple choice changes
483+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
484+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 1 , nsMap ) ;
485+ ChoiceSelectionService . clearChoiceSelection ( doc , choiceField , nsMap ) ;
486+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
487+
488+ // Substitution survived all transitions
489+ expect ( abstractField . selectedMemberIndex ) . toBe ( smsIndex ) ;
490+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
491+ } ) ;
492+ } ) ;
493+
494+ describe ( 'negative cases — sibling overrides preserved' , ( ) => {
495+ it ( 'should preserve all substitutions — both descendant and sibling — on choice change' , ( ) => {
496+ const doc = createChoiceWithAbstractDoc ( ) ;
497+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
498+ const choiceField = findChoiceField ( doc ) ;
499+ const abstractField = findAbstractField ( choiceField ) ;
500+
501+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
502+
503+ // Manually add a sibling substitution entry (simulating another abstract field outside this choice)
504+ const siblingPath = '/ca:Notification/ca:SiblingAbstract' ;
505+ doc . definition . fieldSubstitutions ! . push ( {
506+ schemaPath : siblingPath ,
507+ name : 'ca:SiblingType' ,
508+ originalName : 'ca:SiblingAbstract' ,
509+ } ) ;
510+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 2 ) ;
511+
512+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
513+
514+ // Both substitutions preserved
515+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 2 ) ;
516+ } ) ;
517+
518+ it ( 'should not affect sibling type override outside the choice subtree' , ( ) => {
519+ const doc = createChoiceWithAbstractDoc ( ) ;
520+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
521+ const choiceField = findChoiceField ( doc ) ;
522+
523+ // Manually add a type override for a sibling field (id field, outside the choice)
524+ const root = doc . fields [ 0 ] ;
525+ const idField = root . fields . find ( ( f ) => f . name === 'id' ) ;
526+ if ( ! idField ) throw new Error ( 'id field not found' ) ;
527+ const idPath = SchemaPathService . build ( idField , nsMap ) ;
528+ idField . typeOverride = FieldOverrideVariant . SAFE ;
529+ idField . originalField = {
530+ name : idField . name ,
531+ displayName : idField . name ,
532+ namespaceURI : '' ,
533+ namespacePrefix : null ,
534+ type : idField . type ,
535+ typeQName : null ,
536+ namedTypeFragmentRefs : [ ] ,
537+ } ;
538+ doc . definition . fieldTypeOverrides = [
539+ { schemaPath : idPath , type : 'xs:int' , originalType : 'xs:string' , variant : FieldOverrideVariant . SAFE } ,
540+ ] ;
541+
542+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
543+
544+ // Sibling type override should be preserved
545+ expect ( doc . definition . fieldTypeOverrides ) . toHaveLength ( 1 ) ;
546+ expect ( idField . typeOverride ) . toBe ( FieldOverrideVariant . SAFE ) ;
547+ } ) ;
230548 } ) ;
231549 } ) ;
232550} ) ;
0 commit comments