1
- import { OPENROSA_XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts' ;
1
+ import {
2
+ OPENROSA_XFORMS_NAMESPACE_URI ,
3
+ OPENROSA_XFORMS_PREFIX ,
4
+ XFORMS_NAMESPACE_URI ,
5
+ } from '@getodk/common/constants/xmlns.ts' ;
2
6
import {
3
7
bind ,
4
8
body ,
@@ -18,7 +22,7 @@ import {
18
22
import { TagXFormsElement } from '@getodk/common/test/fixtures/xform-dsl/TagXFormsElement.ts' ;
19
23
import type { XFormsElement } from '@getodk/common/test/fixtures/xform-dsl/XFormsElement.ts' ;
20
24
import { createUniqueId } from 'solid-js' ;
21
- import { beforeEach , describe , expect , it } from 'vitest' ;
25
+ import { assert , beforeEach , describe , expect , it } from 'vitest' ;
22
26
import { Scenario } from '../src/jr/Scenario.ts' ;
23
27
import { ANSWER_OK , ANSWER_REQUIRED_BUT_EMPTY } from '../src/jr/validation/ValidateOutcome.ts' ;
24
28
import { ReactiveScenario } from '../src/reactive/ReactiveScenario.ts' ;
@@ -906,4 +910,245 @@ describe('Form submission', () => {
906
910
907
911
describe . todo ( 'for multiple requests, chunked by maximum size' ) ;
908
912
} ) ;
913
+
914
+ describe ( 'submission-specific metadata' , ( ) => {
915
+ type MetadataElementName = 'instanceID' ;
916
+
917
+ type MetaNamespaceURI = OPENROSA_XFORMS_NAMESPACE_URI | XFORMS_NAMESPACE_URI ;
918
+
919
+ type MetadataValueAssertion = ( value : string | null ) => unknown ;
920
+
921
+ const getMetaChildElement = (
922
+ parent : ParentNode | null ,
923
+ namespaceURI : MetaNamespaceURI ,
924
+ localName : string
925
+ ) : Element | null => {
926
+ if ( parent == null ) {
927
+ return null ;
928
+ }
929
+
930
+ for ( const child of parent . children ) {
931
+ if ( child . namespaceURI === namespaceURI && child . localName === localName ) {
932
+ return child ;
933
+ }
934
+ }
935
+
936
+ return null ;
937
+ } ;
938
+
939
+ /**
940
+ * Normally this might be implemented as a
941
+ * {@link https://vitest.dev/guide/extending-matchers | custom "matcher" (assertion)}.
942
+ * But it's so specific to this sub-suite that it would be silly to sprawl
943
+ * it out into other parts of the codebase!
944
+ */
945
+ const assertMetadata = (
946
+ scenario : Scenario ,
947
+ metaNamespaceURI : MetaNamespaceURI ,
948
+ name : MetadataElementName ,
949
+ assertion : MetadataValueAssertion
950
+ ) : void => {
951
+ const serializedInstanceBody = scenario . proposed_serializeInstance ( ) ;
952
+ /**
953
+ * Important: we intentionally omit the default namespace when serializing instance XML. We need to restore it here to reliably traverse nodes when {@link metaNamespaceURI} is {@link XFORMS_NAMESPACE_URI}.
954
+ */
955
+ const instanceXML = `<instance xmlns="${ XFORMS_NAMESPACE_URI } ">${ serializedInstanceBody } </instance>` ;
956
+
957
+ const parser = new DOMParser ( ) ;
958
+ const instanceDocument = parser . parseFromString ( instanceXML , 'text/xml' ) ;
959
+ const instanceElement = instanceDocument . documentElement ;
960
+ const instanceRoot = instanceElement . firstElementChild ;
961
+
962
+ assert (
963
+ instanceRoot != null ,
964
+ `Failed to find instance root element.\n\nActual serialized XML: ${ serializedInstanceBody } \n\nActual instance DOM state: ${ instanceElement . outerHTML } `
965
+ ) ;
966
+
967
+ const meta = getMetaChildElement ( instanceRoot , metaNamespaceURI , 'meta' ) ;
968
+ const targetElement = getMetaChildElement ( meta , metaNamespaceURI , name ) ;
969
+ const value = targetElement ?. textContent ?? null ;
970
+
971
+ assertion ( value ) ;
972
+ } ;
973
+
974
+ const PRELOAD_UID_PATTERN =
975
+ / ^ u u i d : [ 0 - 9 a - f A - F ] { 8 } - [ 0 - 9 a - f A - F ] { 4 } - [ 1 - 5 ] [ 0 - 9 a - f A - F ] { 3 } - [ 8 9 a b A B ] [ 0 - 9 a - f A - F ] { 3 } - [ 0 - 9 a - f A - F ] { 12 } $ / ;
976
+
977
+ const assertPreloadUIDValue = ( value : string | null ) => {
978
+ assert ( value != null , 'Expected preload uid value to be serialized' ) ;
979
+ expect ( value , 'Expected preload uid value to match pattern' ) . toMatch ( PRELOAD_UID_PATTERN ) ;
980
+ } ;
981
+
982
+ describe ( 'instanceID' , ( ) => {
983
+ describe ( 'preload="uid"' , ( ) => {
984
+ let scenario : Scenario ;
985
+
986
+ beforeEach ( async ( ) => {
987
+ // prettier-ignore
988
+ scenario = await Scenario . init ( 'Meta instanceID preload uid' , html (
989
+ head (
990
+ title ( 'Meta instanceID preload uid' ) ,
991
+ model (
992
+ mainInstance (
993
+ t ( 'data id="meta-instanceid-preload-uid"' ,
994
+ t ( 'inp' , 'inp default value' ) ,
995
+ /** @see note on `namespaces` sub-suite! */
996
+ t ( 'meta' ,
997
+ t ( 'instanceID' ) ) )
998
+ ) ,
999
+ bind ( '/data/inp' ) . type ( 'string' ) ,
1000
+ bind ( '/data/meta/instanceID' ) . preload ( 'uid' )
1001
+ )
1002
+ ) ,
1003
+ body (
1004
+ input ( '/data/inp' ,
1005
+ label ( 'inp' ) ) )
1006
+ ) ) ;
1007
+ } ) ;
1008
+
1009
+ /**
1010
+ * @see {@link https://getodk.github.io/xforms-spec/#preload-attributes:~:text=concatenation%20of%20%E2%80%98uuid%3A%E2%80%99%20and%20uuid() }
1011
+ */
1012
+ it ( 'is populated with a concatenation of ‘uuid:’ and uuid()' , ( ) => {
1013
+ assertMetadata (
1014
+ scenario ,
1015
+ /** @see note on `namespaces` sub-suite! */
1016
+ XFORMS_NAMESPACE_URI ,
1017
+ 'instanceID' ,
1018
+ assertPreloadUIDValue
1019
+ ) ;
1020
+ } ) ;
1021
+
1022
+ it ( 'does not change after an input value is changed' , ( ) => {
1023
+ scenario . answer ( '/data/inp' , 'any non-default value!' ) ;
1024
+
1025
+ assertMetadata (
1026
+ scenario ,
1027
+ /** @see note on `namespaces` sub-suite! */
1028
+ XFORMS_NAMESPACE_URI ,
1029
+ 'instanceID' ,
1030
+ assertPreloadUIDValue
1031
+ ) ;
1032
+ } ) ;
1033
+ } ) ;
1034
+
1035
+ /**
1036
+ * NOTE: Do not read too much intent into this sub-suite coming after
1037
+ * tests above with `meta` and `instanceID` in the default (XForms)
1038
+ * namespace! Those tests were added first because they'll require the
1039
+ * least work to make pass. The `orx` namespace _is preferred_, {@link
1040
+ * https://getodk.github.io/xforms-spec/#metadata | per spec}.
1041
+ *
1042
+ * This fact is further emphasized by the next sub-suite, exercising
1043
+ * default behavior when a `meta` subtree node (of either namespace) is
1044
+ * not present.
1045
+ */
1046
+ describe ( 'namespaces' , ( ) => {
1047
+ it ( `preserves the ${ OPENROSA_XFORMS_PREFIX } (${ OPENROSA_XFORMS_NAMESPACE_URI } ) namespace when used in the form definition` , async ( ) => {
1048
+ // prettier-ignore
1049
+ const scenario = await Scenario . init (
1050
+ 'ORX Meta ORX instanceID preload uid' ,
1051
+ html (
1052
+ head (
1053
+ title ( 'ORX Meta ORX instanceID preload uid' ) ,
1054
+ model (
1055
+ mainInstance (
1056
+ t ( 'data id="orx-meta-instanceid-preload-uid"' ,
1057
+ t ( 'inp' , 'inp default value' ) ,
1058
+ t ( 'orx:meta' ,
1059
+ t ( 'orx:instanceID' ) )
1060
+ )
1061
+ ) ,
1062
+ bind ( '/data/inp' ) . type ( 'string' ) ,
1063
+ bind ( '/data/orx:meta/orx:instanceID' ) . preload ( 'uid' )
1064
+ )
1065
+ ) ,
1066
+ body (
1067
+ input ( '/data/inp' ,
1068
+ label ( 'inp' ) ) )
1069
+ ) ) ;
1070
+
1071
+ assertMetadata (
1072
+ scenario ,
1073
+ OPENROSA_XFORMS_NAMESPACE_URI ,
1074
+ 'instanceID' ,
1075
+ assertPreloadUIDValue
1076
+ ) ;
1077
+ } ) ;
1078
+
1079
+ // This is redundant to other tests already exercising unprefixed names!
1080
+ it . skip ( 'preserves the default/un-prefixed namespace when used in the form definition' ) ;
1081
+ } ) ;
1082
+
1083
+ describe ( 'defaults when absent in form definition' , ( ) => {
1084
+ interface MissingInstanceIDLeafNodeCase {
1085
+ readonly metaNamespaceURI : MetaNamespaceURI ;
1086
+ }
1087
+
1088
+ describe . each < MissingInstanceIDLeafNodeCase > ( [
1089
+ { metaNamespaceURI : OPENROSA_XFORMS_NAMESPACE_URI } ,
1090
+ { metaNamespaceURI : XFORMS_NAMESPACE_URI } ,
1091
+ ] ) ( 'meta namespace URI: $metaNamespaceURI' , ( { metaNamespaceURI } ) => {
1092
+ const expectedNamePrefix =
1093
+ metaNamespaceURI === OPENROSA_XFORMS_NAMESPACE_URI ? 'orx:' : '' ;
1094
+ const metaSubtreeName = `${ expectedNamePrefix } meta` ;
1095
+ const instanceIDName = `${ expectedNamePrefix } instanceID` ;
1096
+
1097
+ it ( `injects and populates a missing ${ instanceIDName } leaf node in an existing ${ metaSubtreeName } subtree` , async ( ) => {
1098
+ // prettier-ignore
1099
+ const scenario = await Scenario . init (
1100
+ 'ORX Meta ORX instanceID preload uid' ,
1101
+ html (
1102
+ head (
1103
+ title ( 'ORX Meta ORX instanceID preload uid' ) ,
1104
+ model (
1105
+ mainInstance (
1106
+ t ( 'data id="orx-meta-instanceid-preload-uid"' ,
1107
+ t ( 'inp' , 'inp default value' ) ,
1108
+ t ( metaSubtreeName )
1109
+ )
1110
+ ) ,
1111
+ bind ( '/data/inp' ) . type ( 'string' )
1112
+ )
1113
+ ) ,
1114
+ body (
1115
+ input ( '/data/inp' ,
1116
+ label ( 'inp' ) ) )
1117
+ ) ) ;
1118
+
1119
+ assertMetadata ( scenario , metaNamespaceURI , 'instanceID' , assertPreloadUIDValue ) ;
1120
+ } ) ;
1121
+ } ) ;
1122
+
1123
+ it ( 'injects and populates an orx:meta subtree AND orx:instanceID leaf node' , async ( ) => {
1124
+ // prettier-ignore
1125
+ const scenario = await Scenario . init (
1126
+ 'ORX Meta ORX instanceID preload uid' ,
1127
+ html (
1128
+ head (
1129
+ title ( 'ORX Meta ORX instanceID preload uid' ) ,
1130
+ model (
1131
+ mainInstance (
1132
+ t ( 'data id="orx-meta-instanceid-preload-uid"' ,
1133
+ t ( 'inp' , 'inp default value' )
1134
+ )
1135
+ ) ,
1136
+ bind ( '/data/inp' ) . type ( 'string' )
1137
+ )
1138
+ ) ,
1139
+ body (
1140
+ input ( '/data/inp' ,
1141
+ label ( 'inp' ) ) )
1142
+ ) ) ;
1143
+
1144
+ assertMetadata (
1145
+ scenario ,
1146
+ OPENROSA_XFORMS_NAMESPACE_URI ,
1147
+ 'instanceID' ,
1148
+ assertPreloadUIDValue
1149
+ ) ;
1150
+ } ) ;
1151
+ } ) ;
1152
+ } ) ;
1153
+ } ) ;
909
1154
} ) ;
0 commit comments