Skip to content

Commit 0287a16

Browse files
engine (fix): include namespace declarations in submission XML
1 parent a08e77b commit 0287a16

File tree

9 files changed

+230
-80
lines changed

9 files changed

+230
-80
lines changed

.changeset/healthy-jobs-look.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@getodk/xforms-engine': patch
3+
'@getodk/web-forms': patch
4+
'@getodk/scenario': patch
5+
---
6+
7+
Fix: include namespace declarations in submission XML

packages/scenario/src/serialization/ComparableXMLSerialization.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
1+
import {
2+
XFORMS_NAMESPACE_URI,
3+
XMLNS_NAMESPACE_URI,
4+
XMLNS_PREFIX,
5+
} from '@getodk/common/constants/xmlns.ts';
26
import { InspectableComparisonError } from '@getodk/common/test/assertions/helpers.ts';
37
import type { SimpleAssertionResult } from '@getodk/common/test/assertions/vitest/shared-extension-types.ts';
48
import { ComparableAssertableValue } from '../comparable/ComparableAssertableValue.ts';
@@ -8,38 +12,53 @@ class ComparableXMLQualifiedName {
812

913
constructor(
1014
readonly namespaceURI: string | null,
15+
readonly nodeName: string,
1116
readonly localName: string
1217
) {
13-
this.sortKey = JSON.stringify({ namespaceURI, localName });
18+
let namespaceDeclarationType: string;
19+
20+
if (namespaceURI === XMLNS_NAMESPACE_URI) {
21+
if (nodeName === XMLNS_PREFIX) {
22+
namespaceDeclarationType = 'default';
23+
} else {
24+
namespaceDeclarationType = 'non-default';
25+
}
26+
} else {
27+
namespaceDeclarationType = 'none';
28+
}
29+
30+
this.sortKey = JSON.stringify({
31+
namespaceDeclarationType,
32+
namespaceURI,
33+
localName,
34+
});
1435
}
1536

16-
/**
17-
* @todo prefix re-serialization
18-
*/
1937
toString(): string {
20-
const { namespaceURI } = this;
38+
const { namespaceURI, nodeName } = this;
2139

2240
if (namespaceURI == null || namespaceURI === XFORMS_NAMESPACE_URI) {
2341
return this.localName;
2442
}
2543

26-
return this.sortKey;
44+
return nodeName;
2745
}
2846
}
2947

3048
class ComparableXMLAttribute {
3149
static from(attr: Attr): ComparableXMLAttribute {
32-
return new this(attr.namespaceURI, attr.localName, attr.value);
50+
return new this(attr.namespaceURI, attr.nodeName, attr.localName, attr.value);
3351
}
3452

3553
readonly qualifiedName: ComparableXMLQualifiedName;
3654

3755
private constructor(
3856
namespaceURI: string | null,
57+
nodeName: string,
3958
localName: string,
4059
readonly value: string
4160
) {
42-
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, localName);
61+
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, nodeName, localName);
4362
}
4463

4564
/**
@@ -59,17 +78,23 @@ const comparableXMLElementAttributes = (element: Element): readonly ComparableXM
5978
return ComparableXMLAttribute.from(attr);
6079
});
6180

62-
return attributes.sort(({ qualifiedName: a }, { qualifiedName: b }) => {
63-
if (a > b) {
64-
return 1;
81+
return attributes.sort(
82+
(
83+
// prettier-ignore
84+
{ qualifiedName: { sortKey: a } },
85+
{ qualifiedName: { sortKey: b } }
86+
) => {
87+
if (a > b) {
88+
return 1;
89+
}
90+
91+
if (b > a) {
92+
return -1;
93+
}
94+
95+
return 0;
6596
}
66-
67-
if (b > a) {
68-
return -1;
69-
}
70-
71-
return 0;
72-
});
97+
);
7398
};
7499

75100
const isElement = (node: ChildNode): node is Element => {
@@ -118,18 +143,25 @@ class ComparableXMLElement {
118143
const attributes = comparableXMLElementAttributes(element);
119144
const children = comparableXMLElementChildren(element);
120145

121-
return new this(element.namespaceURI, element.localName, attributes, children);
146+
return new this(
147+
element.namespaceURI,
148+
element.nodeName,
149+
element.localName,
150+
attributes,
151+
children
152+
);
122153
}
123154

124155
readonly qualifiedName: ComparableXMLQualifiedName;
125156

126157
private constructor(
127158
namespaceURI: string | null,
159+
nodeName: string,
128160
localName: string,
129161
readonly attributes: readonly ComparableXMLAttribute[],
130162
readonly children: readonly ComparableXMLElementChild[]
131163
) {
132-
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, localName);
164+
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, nodeName, localName);
133165
}
134166

135167
toString(): string {

packages/scenario/test/submission.test.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
OPENROSA_XFORMS_NAMESPACE_URI,
3+
XFORMS_NAMESPACE_URI,
4+
} from '@getodk/common/constants/xmlns.ts';
15
import {
26
bind,
37
body,
@@ -141,7 +145,7 @@ describe('Form submission', () => {
141145

142146
expect(scenario).toHaveSerializedSubmissionXML(
143147
// prettier-ignore
144-
t('data id="xml-serialization-basic-default-values"',
148+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-basic-default-values"`,
145149
t('grp',
146150
t('inp', defaults.inp ?? ''),
147151
t('sel1', defaults.sel1 ?? ''),
@@ -186,7 +190,7 @@ describe('Form submission', () => {
186190

187191
expect(scenario).toHaveSerializedSubmissionXML(
188192
// prettier-ignore
189-
t(`data id="${formId}" version="${version}"`,
193+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="${formId}" version="${version}"`,
190194
t('inp', 'val'),
191195
t('meta',
192196
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
@@ -222,7 +226,7 @@ describe('Form submission', () => {
222226

223227
expect(scenario).toHaveSerializedSubmissionXML(
224228
// prettier-ignore
225-
t(`data id="${formId}" orx:version="${version}"`,
229+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" xmlns:orx="${OPENROSA_XFORMS_NAMESPACE_URI}" id="${formId}" orx:version="${version}"`,
226230
t('inp', 'val'),
227231
t('meta',
228232
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
@@ -258,7 +262,7 @@ describe('Form submission', () => {
258262

259263
expect(scenario).toHaveSerializedSubmissionXML(
260264
// prettier-ignore
261-
t(`data id="${formId}" orx:version="${version}"`,
265+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" xmlns:orx="${OPENROSA_XFORMS_NAMESPACE_URI}" id="${formId}" orx:version="${version}"`,
262266
t('inp', 'val'),
263267
t('meta',
264268
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
@@ -312,7 +316,7 @@ describe('Form submission', () => {
312316

313317
expect(scenario).toHaveSerializedSubmissionXML(
314318
// prettier-ignore
315-
t('data id="unicode-normalization"',
319+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="unicode-normalization"`,
316320
t('rep',
317321
t('inp', composed)),
318322
t('meta',
@@ -327,7 +331,7 @@ describe('Form submission', () => {
327331

328332
expect(scenario).toHaveSerializedSubmissionXML(
329333
// prettier-ignore
330-
t('data id="unicode-normalization"',
334+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="unicode-normalization"`,
331335
t('rep',
332336
t('inp', composed)),
333337
t('meta',
@@ -365,7 +369,7 @@ describe('Form submission', () => {
365369
it('does not serialize an element for a repeat range', () => {
366370
expect(scenario).toHaveSerializedSubmissionXML(
367371
// prettier-ignore
368-
t('data id="xml-serialization-repeats"',
372+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-repeats"`,
369373
t('meta',
370374
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
371375
);
@@ -379,7 +383,7 @@ describe('Form submission', () => {
379383

380384
expect(scenario).toHaveSerializedSubmissionXML(
381385
// prettier-ignore
382-
t('data id="xml-serialization-repeats"',
386+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-repeats"`,
383387
t('rep',
384388
t('inp', 'a')),
385389
t('rep',
@@ -392,7 +396,7 @@ describe('Form submission', () => {
392396

393397
expect(scenario).toHaveSerializedSubmissionXML(
394398
// prettier-ignore
395-
t('data id="xml-serialization-repeats"',
399+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-repeats"`,
396400
t('rep',
397401
t('inp', 'b')),
398402
t('meta',
@@ -443,7 +447,7 @@ describe('Form submission', () => {
443447

444448
expect(scenario).toHaveSerializedSubmissionXML(
445449
// prettier-ignore
446-
t('data id="xml-serialization-relevance"',
450+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-relevance"`,
447451
t('grp-rel', '1'),
448452
t('inp-rel', '0'),
449453
t('grp'),
@@ -457,7 +461,7 @@ describe('Form submission', () => {
457461

458462
expect(scenario).toHaveSerializedSubmissionXML(
459463
// prettier-ignore
460-
t('data id="xml-serialization-relevance"',
464+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-relevance"`,
461465
t('grp-rel', '0'),
462466
t('inp-rel', '1'),
463467
t('meta',
@@ -510,7 +514,7 @@ describe('Form submission', () => {
510514
// Default serialization before any state change
511515
expect(serialized).toBe(
512516
// prettier-ignore
513-
t('data id="reactive-xml-serialization"',
517+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
514518
t('rep-inp-rel'),
515519
t('rep',
516520
t('inp')),
@@ -525,7 +529,7 @@ describe('Form submission', () => {
525529
// After first value change
526530
expect(serialized).toBe(
527531
// prettier-ignore
528-
t('data id="reactive-xml-serialization"',
532+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
529533
t('rep-inp-rel'),
530534
t('rep',
531535
t('inp', `${i}`)),
@@ -545,7 +549,7 @@ describe('Form submission', () => {
545549
// Default serialization before any state change
546550
expect(serialized).toBe(
547551
// prettier-ignore
548-
t('data id="reactive-xml-serialization"',
552+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
549553
t('rep-inp-rel'),
550554
t('rep',
551555
t('inp')),
@@ -558,7 +562,7 @@ describe('Form submission', () => {
558562
// First repeat instance added
559563
expect(serialized).toBe(
560564
// prettier-ignore
561-
t('data id="reactive-xml-serialization"',
565+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
562566
t('rep-inp-rel'),
563567
t('rep',
564568
t('inp')),
@@ -573,7 +577,7 @@ describe('Form submission', () => {
573577
// Second repeat instance added
574578
expect(serialized).toBe(
575579
// prettier-ignore
576-
t('data id="reactive-xml-serialization"',
580+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
577581
t('rep-inp-rel'),
578582
t('rep',
579583
t('inp')),
@@ -592,7 +596,7 @@ describe('Form submission', () => {
592596
// Each of the above values set
593597
expect(serialized).toBe(
594598
// prettier-ignore
595-
t('data id="reactive-xml-serialization"',
599+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
596600
t('rep-inp-rel'),
597601
t('rep',
598602
t('inp', 'rep 1 inp')),
@@ -609,7 +613,7 @@ describe('Form submission', () => {
609613
// Last repeat instance removed
610614
expect(serialized).toBe(
611615
// prettier-ignore
612-
t('data id="reactive-xml-serialization"',
616+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
613617
t('rep-inp-rel'),
614618
t('rep',
615619
t('inp', 'rep 1 inp')),
@@ -624,7 +628,7 @@ describe('Form submission', () => {
624628
// First repeat instance removed
625629
expect(serialized).toBe(
626630
// prettier-ignore
627-
t('data id="reactive-xml-serialization"',
631+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
628632
t('rep-inp-rel'),
629633
t('rep',
630634
t('inp', 'rep 2 inp')),
@@ -637,7 +641,7 @@ describe('Form submission', () => {
637641
// All repeat instances removed
638642
expect(serialized).toBe(
639643
// prettier-ignore
640-
t('data id="reactive-xml-serialization"',
644+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
641645
t('rep-inp-rel'),
642646
t('meta',
643647
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
@@ -660,7 +664,7 @@ describe('Form submission', () => {
660664
// Current serialization before any relevance change
661665
expect(serialized).toBe(
662666
// prettier-ignore
663-
t('data id="reactive-xml-serialization"',
667+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
664668
t('rep-inp-rel'),
665669
t('rep',
666670
t('inp', 'rep 1 inp')),
@@ -677,7 +681,7 @@ describe('Form submission', () => {
677681
// Non-relevant /data/rep[position() != '1']/inp omitted
678682
expect(serialized).toBe(
679683
// prettier-ignore
680-
t('data id="reactive-xml-serialization"',
684+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
681685
t('rep-inp-rel', '1'),
682686
t('rep',
683687
t('inp', 'rep 1 inp')),
@@ -692,7 +696,7 @@ describe('Form submission', () => {
692696
// Non-relevant /data/rep[position() != '3']/inp omitted
693697
expect(serialized).toBe(
694698
// prettier-ignore
695-
t('data id="reactive-xml-serialization"',
699+
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
696700
t('rep-inp-rel', '3'),
697701
t('rep'),
698702
t('rep'),
@@ -842,7 +846,7 @@ describe('Form submission', () => {
842846
expect(scenario.getValidationOutcome().outcome).toBe(ANSWER_OK);
843847

844848
// prettier-ignore
845-
validSubmissionXML = t('data id="prepare-for-submission"',
849+
validSubmissionXML = t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="prepare-for-submission"`,
846850
t('rep',
847851
t('inp', 'rep 1 inp')),
848852
t('rep',
@@ -879,7 +883,7 @@ describe('Form submission', () => {
879883
expect(scenario.getValidationOutcome().outcome).toBe(ANSWER_REQUIRED_BUT_EMPTY);
880884

881885
// prettier-ignore
882-
invalidSubmissionXML = t('data id="prepare-for-submission"',
886+
invalidSubmissionXML = t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="prepare-for-submission"`,
883887
t('rep',
884888
t('inp', 'rep 1 inp')),
885889
t('rep',

packages/xforms-engine/src/lib/client-reactivity/submission/createRootSubmissionState.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { serializeParentElementXML } from '../../xml-serialization.ts';
55
export const createRootSubmissionState = (node: Root): SubmissionState => {
66
return {
77
get submissionXML() {
8+
const { namespaceDeclarations, attributes } = node.definition;
89
const serializedChildren = node.currentState.children.map((child) => {
910
return child.submissionState.submissionXML;
1011
});
1112

1213
return serializeParentElementXML(node.definition.nodeName, serializedChildren, {
13-
attributes: node.definition.attributes,
14+
namespaceDeclarations,
15+
attributes,
1416
});
1517
},
1618
};

0 commit comments

Comments
 (0)