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' ;
711
812describe ( 'ChoiceSelectionService' , ( ) => {
913 let document : XmlSchemaDocument ;
@@ -145,7 +149,7 @@ describe('ChoiceSelectionService', () => {
145149 expect ( document . definition . choiceSelections ?. length ) . toBe ( 2 ) ;
146150 } ) ;
147151
148- it ( 'should cascade invalidate true descendant selections' , ( ) => {
152+ it ( 'should preserve descendant choice selections' , ( ) => {
149153 const parentChoice = document . fields [ 0 ] . fields [ 0 ] ;
150154 const emailMember = parentChoice . fields [ 0 ] ;
151155 const nestedChoice = emailMember . fields [ 0 ] ;
@@ -155,9 +159,9 @@ describe('ChoiceSelectionService', () => {
155159
156160 ChoiceSelectionService . setChoiceSelection ( document , parentChoice , 1 , namespaceMap ) ;
157161
158- expect ( document . definition . choiceSelections ?. length ) . toBe ( 1 ) ;
159- expect ( document . definition . choiceSelections ?. [ 0 ] . schemaPath ) . not . toContain ( 'email' ) ;
160- expect ( nestedChoice . selectedMemberIndex ) . toBeUndefined ( ) ;
162+ // Both parent and nested selections preserved
163+ expect ( document . definition . choiceSelections ?. length ) . toBe ( 2 ) ;
164+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 0 ) ;
161165 } ) ;
162166 } ) ;
163167
@@ -214,7 +218,7 @@ describe('ChoiceSelectionService', () => {
214218 expect ( document . definition . choiceSelections ?. [ 0 ] . schemaPath ) . toContain ( 'Container' ) ;
215219 } ) ;
216220
217- it ( 'should cascade invalidate true descendant selections on clear' , ( ) => {
221+ it ( 'should preserve descendant choice selections on clear' , ( ) => {
218222 const parentChoice = document . fields [ 0 ] . fields [ 0 ] ;
219223 const emailMember = parentChoice . fields [ 0 ] ;
220224 const nestedChoice = emailMember . fields [ 0 ] ;
@@ -225,8 +229,291 @@ describe('ChoiceSelectionService', () => {
225229
226230 ChoiceSelectionService . clearChoiceSelection ( document , parentChoice , namespaceMap ) ;
227231
228- expect ( document . definition . choiceSelections ?. length ) . toBe ( 0 ) ;
229- expect ( nestedChoice . selectedMemberIndex ) . toBeUndefined ( ) ;
232+ // Nested selection preserved
233+ expect ( document . definition . choiceSelections ?. length ) . toBe ( 1 ) ;
234+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 1 ) ;
235+ } ) ;
236+ } ) ;
237+
238+ describe ( 'choice + descendant override interaction (issue #3234)' , ( ) => {
239+ const NS_CHOICE_ABSTRACT = 'http://www.example.com/CHOICE_ABSTRACT' ;
240+
241+ function createChoiceWithAbstractDoc ( ) {
242+ const definition = new DocumentDefinition (
243+ DocumentType . SOURCE_BODY ,
244+ DocumentDefinitionType . XML_SCHEMA ,
245+ 'test-doc' ,
246+ { 'ChoiceWithAbstract.xsd' : getChoiceWithAbstractXsd ( ) } ,
247+ ) ;
248+ definition . rootElementChoice = { namespaceUri : NS_CHOICE_ABSTRACT , name : 'Notification' } ;
249+ const result = XmlSchemaDocumentService . createXmlSchemaDocument ( definition ) ;
250+ if ( result . validationStatus !== 'success' || ! result . document ) {
251+ throw new Error (
252+ result . errors ?. map ( ( e ) => e . message ) . join ( '; ' ) || 'Failed to create choice+abstract test document' ,
253+ ) ;
254+ }
255+ return result . document ;
256+ }
257+
258+ function findChoiceField ( doc : XmlSchemaDocument ) : XmlSchemaField {
259+ const root = doc . fields [ 0 ] ;
260+ const choice = root . fields . find ( ( f ) => f . wrapperKind === 'choice' ) ;
261+ if ( ! choice ) throw new Error ( 'Choice field not found in Notification' ) ;
262+ return choice ;
263+ }
264+
265+ function findAbstractField ( choiceField : IField ) : XmlSchemaField {
266+ const abstract = choiceField . fields . find ( ( f ) => f . wrapperKind === 'abstract' ) ;
267+ if ( ! abstract ) throw new Error ( 'Abstract field not found inside choice' ) ;
268+ return abstract as XmlSchemaField ;
269+ }
270+
271+ describe ( 'abstract substitution preserved across choice changes' , ( ) => {
272+ it ( 'should preserve descendant abstract substitution when setting choice selection' , ( ) => {
273+ const doc = createChoiceWithAbstractDoc ( ) ;
274+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
275+ const choiceField = findChoiceField ( doc ) ;
276+ const abstractField = findAbstractField ( choiceField ) ;
277+
278+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
279+ const emailIndex = abstractField . selectedMemberIndex ;
280+ expect ( emailIndex ) . toBeDefined ( ) ;
281+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
282+
283+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
284+
285+ // Substitution stays in both definition and live field — consistent state
286+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
287+ expect ( abstractField . selectedMemberIndex ) . toBe ( emailIndex ) ;
288+ } ) ;
289+
290+ it ( 'should preserve descendant abstract substitution when clearing choice selection' , ( ) => {
291+ const doc = createChoiceWithAbstractDoc ( ) ;
292+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
293+ const choiceField = findChoiceField ( doc ) ;
294+ const abstractField = findAbstractField ( choiceField ) ;
295+
296+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
297+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
298+ const smsIndex = abstractField . selectedMemberIndex ;
299+ expect ( smsIndex ) . toBeDefined ( ) ;
300+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
301+
302+ ChoiceSelectionService . clearChoiceSelection ( doc , choiceField , nsMap ) ;
303+
304+ // Substitution preserved
305+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
306+ expect ( abstractField . selectedMemberIndex ) . toBe ( smsIndex ) ;
307+ } ) ;
308+
309+ it ( 'should allow reverting abstract substitution after parent choice selection (issue #3234)' , ( ) => {
310+ const doc = createChoiceWithAbstractDoc ( ) ;
311+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
312+ const choiceField = findChoiceField ( doc ) ;
313+ const abstractField = findAbstractField ( choiceField ) ;
314+
315+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
316+ expect ( abstractField . selectedMemberIndex ) . toBeDefined ( ) ;
317+
318+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
319+
320+ // Substitution is preserved, so revert should work
321+ FieldOverrideService . revertFieldSubstitution ( abstractField , nsMap ) ;
322+
323+ expect ( abstractField . selectedMemberIndex ) . toBeUndefined ( ) ;
324+ expect ( doc . definition . fieldSubstitutions ?? [ ] ) . toHaveLength ( 0 ) ;
325+ } ) ;
326+
327+ it ( 'should allow switching substitution after choice selection' , ( ) => {
328+ const doc = createChoiceWithAbstractDoc ( ) ;
329+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
330+ const choiceField = findChoiceField ( doc ) ;
331+ const abstractField = findAbstractField ( choiceField ) ;
332+
333+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
334+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
335+
336+ // Switch from Email to SMS
337+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
338+
339+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
340+ expect ( doc . definition . fieldSubstitutions ! [ 0 ] . name ) . toBe ( 'ca:SMS' ) ;
341+ const smsIndex = abstractField . fields . findIndex ( ( f ) => f . name === 'SMS' ) ;
342+ expect ( abstractField . selectedMemberIndex ) . toBe ( smsIndex ) ;
343+ } ) ;
344+
345+ it ( 'should allow revert after switching substitution across choice changes' , ( ) => {
346+ const doc = createChoiceWithAbstractDoc ( ) ;
347+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
348+ const choiceField = findChoiceField ( doc ) ;
349+ const abstractField = findAbstractField ( choiceField ) ;
350+
351+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
352+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
353+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
354+
355+ FieldOverrideService . revertFieldSubstitution ( abstractField , nsMap ) ;
356+
357+ expect ( abstractField . selectedMemberIndex ) . toBeUndefined ( ) ;
358+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 0 ) ;
359+ } ) ;
360+ } ) ;
361+
362+ describe ( 'type override preserved across choice changes' , ( ) => {
363+ function findDescendantLeaf ( field : IField ) : IField {
364+ let current = field ;
365+ while ( current . fields . length > 0 ) {
366+ current = current . fields [ 0 ] ;
367+ }
368+ return current ;
369+ }
370+
371+ it ( 'should preserve descendant type override when setting choice selection' , ( ) => {
372+ const doc = createChoiceWithAbstractDoc ( ) ;
373+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
374+ const choiceField = findChoiceField ( doc ) ;
375+
376+ const firstMember = choiceField . fields [ 0 ] ;
377+ const leafField = findDescendantLeaf ( firstMember ) ;
378+
379+ const leafPath = SchemaPathService . build ( leafField , nsMap ) ;
380+ leafField . typeOverride = FieldOverrideVariant . SAFE ;
381+ leafField . originalField = { name : leafField . name , type : leafField . type } ;
382+ doc . definition . fieldTypeOverrides = [ { schemaPath : leafPath , typeString : 'xs:int' } ] ;
383+
384+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
385+
386+ expect ( doc . definition . fieldTypeOverrides ) . toHaveLength ( 1 ) ;
387+ expect ( leafField . typeOverride ) . toBe ( FieldOverrideVariant . SAFE ) ;
388+ } ) ;
389+
390+ it ( 'should preserve descendant type override when clearing choice selection' , ( ) => {
391+ const doc = createChoiceWithAbstractDoc ( ) ;
392+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
393+ const choiceField = findChoiceField ( doc ) ;
394+
395+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
396+
397+ const firstMember = choiceField . fields [ 0 ] ;
398+ const leafField = findDescendantLeaf ( firstMember ) ;
399+
400+ const leafPath = SchemaPathService . build ( leafField , nsMap ) ;
401+ leafField . typeOverride = FieldOverrideVariant . SAFE ;
402+ leafField . originalField = { name : leafField . name , type : leafField . type } ;
403+ doc . definition . fieldTypeOverrides = [ { schemaPath : leafPath , typeString : 'xs:int' } ] ;
404+
405+ ChoiceSelectionService . clearChoiceSelection ( doc , choiceField , nsMap ) ;
406+
407+ expect ( doc . definition . fieldTypeOverrides ) . toHaveLength ( 1 ) ;
408+ expect ( leafField . typeOverride ) . toBe ( FieldOverrideVariant . SAFE ) ;
409+ } ) ;
410+ } ) ;
411+
412+ describe ( 'round-trip — overrides survive choice switch and switch-back (issue #3232)' , ( ) => {
413+ it ( 'should preserve nested choice selection across parent choice round-trip' , ( ) => {
414+ const doc = createTestDocumentWithChoices ( ) ;
415+ const nsMap = { xs : NS_XML_SCHEMA , ns0 : 'io.kaoto.test' } ;
416+ const parentChoice = doc . fields [ 0 ] . fields [ 0 ] ; // [email, phone, fax]
417+ const emailMember = parentChoice . fields [ 0 ] ;
418+ const nestedChoice = emailMember . fields [ 0 ] ; // [work, personal]
419+
420+ // Select nested choice inside email
421+ ChoiceSelectionService . setChoiceSelection ( doc , parentChoice , 0 , nsMap ) ;
422+ ChoiceSelectionService . setChoiceSelection ( doc , nestedChoice , 1 , nsMap ) ;
423+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 1 ) ;
424+
425+ // Switch parent away from email
426+ ChoiceSelectionService . setChoiceSelection ( doc , parentChoice , 2 , nsMap ) ;
427+
428+ // Switch parent back to email
429+ ChoiceSelectionService . setChoiceSelection ( doc , parentChoice , 0 , nsMap ) ;
430+
431+ // Nested selection survived the round-trip
432+ expect ( nestedChoice . selectedMemberIndex ) . toBe ( 1 ) ;
433+ expect ( doc . definition . choiceSelections ?. length ) . toBe ( 2 ) ;
434+ } ) ;
435+
436+ it ( 'should preserve substitution across parent choice round-trip' , ( ) => {
437+ const doc = createChoiceWithAbstractDoc ( ) ;
438+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
439+ const choiceField = findChoiceField ( doc ) ;
440+ const abstractField = findAbstractField ( choiceField ) ;
441+
442+ // Select substitution
443+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
444+ const emailIndex = abstractField . selectedMemberIndex ;
445+
446+ // Switch choice away and back
447+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 1 , nsMap ) ;
448+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
449+
450+ // Substitution survived
451+ expect ( abstractField . selectedMemberIndex ) . toBe ( emailIndex ) ;
452+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
453+ expect ( doc . definition . fieldSubstitutions ! [ 0 ] . name ) . toBe ( 'ca:Email' ) ;
454+ } ) ;
455+
456+ it ( 'should preserve substitution through multiple choice changes' , ( ) => {
457+ const doc = createChoiceWithAbstractDoc ( ) ;
458+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
459+ const choiceField = findChoiceField ( doc ) ;
460+ const abstractField = findAbstractField ( choiceField ) ;
461+
462+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:SMS' , nsMap ) ;
463+ const smsIndex = abstractField . selectedMemberIndex ;
464+
465+ // Multiple choice changes
466+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
467+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 1 , nsMap ) ;
468+ ChoiceSelectionService . clearChoiceSelection ( doc , choiceField , nsMap ) ;
469+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
470+
471+ // Substitution survived all transitions
472+ expect ( abstractField . selectedMemberIndex ) . toBe ( smsIndex ) ;
473+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 1 ) ;
474+ } ) ;
475+ } ) ;
476+
477+ describe ( 'negative cases — sibling overrides preserved' , ( ) => {
478+ it ( 'should preserve all substitutions — both descendant and sibling — on choice change' , ( ) => {
479+ const doc = createChoiceWithAbstractDoc ( ) ;
480+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
481+ const choiceField = findChoiceField ( doc ) ;
482+ const abstractField = findAbstractField ( choiceField ) ;
483+
484+ FieldOverrideService . applyFieldSubstitution ( abstractField , 'ca:Email' , nsMap ) ;
485+
486+ // Manually add a sibling substitution entry (simulating another abstract field outside this choice)
487+ const siblingPath = '/ca:Notification/ca:SiblingAbstract' ;
488+ doc . definition . fieldSubstitutions ! . push ( { schemaPath : siblingPath , name : 'ca:SiblingType' } ) ;
489+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 2 ) ;
490+
491+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
492+
493+ // Both substitutions preserved
494+ expect ( doc . definition . fieldSubstitutions ) . toHaveLength ( 2 ) ;
495+ } ) ;
496+
497+ it ( 'should not affect sibling type override outside the choice subtree' , ( ) => {
498+ const doc = createChoiceWithAbstractDoc ( ) ;
499+ const nsMap = { ca : NS_CHOICE_ABSTRACT } ;
500+ const choiceField = findChoiceField ( doc ) ;
501+
502+ // Manually add a type override for a sibling field (id field, outside the choice)
503+ const root = doc . fields [ 0 ] ;
504+ const idField = root . fields . find ( ( f ) => f . name === 'id' ) ;
505+ if ( ! idField ) throw new Error ( 'id field not found' ) ;
506+ const idPath = SchemaPathService . build ( idField , nsMap ) ;
507+ idField . typeOverride = FieldOverrideVariant . SAFE ;
508+ idField . originalField = { name : idField . name , type : idField . type } ;
509+ doc . definition . fieldTypeOverrides = [ { schemaPath : idPath , typeString : 'xs:int' } ] ;
510+
511+ ChoiceSelectionService . setChoiceSelection ( doc , choiceField , 0 , nsMap ) ;
512+
513+ // Sibling type override should be preserved
514+ expect ( doc . definition . fieldTypeOverrides ) . toHaveLength ( 1 ) ;
515+ expect ( idField . typeOverride ) . toBe ( FieldOverrideVariant . SAFE ) ;
516+ } ) ;
230517 } ) ;
231518 } ) ;
232519} ) ;
0 commit comments