diff --git a/.changeset/ninety-pets-post.md b/.changeset/ninety-pets-post.md
new file mode 100644
index 000000000..80752b900
--- /dev/null
+++ b/.changeset/ninety-pets-post.md
@@ -0,0 +1,7 @@
+---
+'@getodk/xforms-engine': patch
+'@getodk/scenario': patch
+'@getodk/web-forms': patch
+---
+
+Set attribute value to instance value when editing submission
diff --git a/packages/scenario/src/client/init.ts b/packages/scenario/src/client/init.ts
index 0f32784cd..127987243 100644
--- a/packages/scenario/src/client/init.ts
+++ b/packages/scenario/src/client/init.ts
@@ -1,5 +1,6 @@
import type { JRResourceService } from '@getodk/common/jr-resources/JRResourceService.ts';
import type {
+ EditFormInstanceInput,
FormResource,
GeolocationProvider,
InstanceAttachmentsConfig,
@@ -11,7 +12,7 @@ import type {
PreloadProperties,
RootNode,
} from '@getodk/xforms-engine';
-import { createInstance } from '@getodk/xforms-engine';
+import { createInstance, editInstance } from '@getodk/xforms-engine';
import type { Owner } from 'solid-js';
import { createRoot } from 'solid-js';
import { getAssertedOwner, runInSolidScope } from './solid-helpers.ts';
@@ -31,6 +32,7 @@ export interface TestFormOptions {
readonly instanceAttachments: InstanceAttachmentsConfig;
readonly preloadProperties: PreloadProperties;
readonly geolocationProvider: GeolocationProvider;
+ readonly editInstance: EditFormInstanceInput | null;
}
const defaultConfig = {
@@ -57,7 +59,7 @@ export const initializeTestForm = async (
const owner = getAssertedOwner();
const { formResult: form, root: instanceRoot } = await runInSolidScope(owner, async () => {
- return createInstance(formResource, {
+ const initOptions = {
form: {
...defaultConfig,
fetchFormAttachment: options.resourceService.handleRequest,
@@ -69,7 +71,11 @@ export const initializeTestForm = async (
preloadProperties: options.preloadProperties,
geolocationProvider: options.geolocationProvider,
},
- });
+ };
+ if (options.editInstance) {
+ return editInstance(formResource, options.editInstance, initOptions);
+ }
+ return createInstance(formResource, initOptions);
});
return {
diff --git a/packages/scenario/src/jr/Scenario.ts b/packages/scenario/src/jr/Scenario.ts
index 64177aedd..47ec338d6 100644
--- a/packages/scenario/src/jr/Scenario.ts
+++ b/packages/scenario/src/jr/Scenario.ts
@@ -179,6 +179,7 @@ export class Scenario {
geolocationProvider: overrideOptions?.geolocationProvider ?? {
getLocation: () => Promise.resolve(''),
},
+ editInstance: overrideOptions?.editInstance ?? null,
};
}
diff --git a/packages/scenario/test/bind-attributes.test.ts b/packages/scenario/test/bind-attributes.test.ts
index 8e1c838e3..e5e241c25 100644
--- a/packages/scenario/test/bind-attributes.test.ts
+++ b/packages/scenario/test/bind-attributes.test.ts
@@ -11,12 +11,15 @@ import {
model,
repeat,
select1,
+ setvalue,
t,
title,
} from '@getodk/common/test/fixtures/xform-dsl/index.ts';
+import { constants, type InstanceData } from '@getodk/xforms-engine';
import { beforeEach, describe, expect, it } from 'vitest';
import { stringAnswer } from '../src/answer/ExpectedStringAnswer.ts';
import { Scenario } from '../src/jr/Scenario.ts';
+const { INSTANCE_FILE_NAME, INSTANCE_FILE_TYPE } = constants;
const IGNORED_INSTANCE_ID = 'ignored for purposes of functionality under test';
@@ -138,6 +141,52 @@ describe('Bind attributes', () => {
});
});
+ it('binds attributes from instance data when editing', async () => {
+ const instanceXML = `
+ cat
+ sammy
+
+ uuid:c0b9c932-e78b-474b-8568-48980113a7ac
+
+ uuid:da0bb609-4293-48bd-ba64-136ffa1c43d3
+
+`;
+ const instanceFile = new File([instanceXML], INSTANCE_FILE_NAME, {
+ type: INSTANCE_FILE_TYPE,
+ });
+ const instanceData = new FormData();
+ instanceData.set(INSTANCE_FILE_NAME, instanceFile);
+ const form = html(
+ head(
+ title('Neighborhood pet: add'),
+ model(
+ mainInstance(
+ t(
+ 'data id="pets_add"',
+ t('species'),
+ t('name'),
+ t('meta', t('entity create="1" dataset="pets" id=""', t('label')))
+ )
+ ),
+ bind('/data/name').type('string'),
+ bind('/data/meta/entity/@create').calculate('1').type('string'),
+ bind('/data/meta/entity/label').calculate('/data/name').type('string'),
+ setvalue('odk-instance-first-load', '/data/meta/entity/@id', 'uuid()')
+ )
+ ),
+ body(input('/data/species'), input('/data/name'))
+ );
+ const scenario = await Scenario.init('upgrade form', form, {
+ editInstance: {
+ inputType: 'FORM_INSTANCE_INPUT_RESOLVED',
+ data: [instanceData as InstanceData],
+ },
+ });
+ expect(scenario.attributeOf('/data/meta/entity', 'id').getValue()).to.equal(
+ '0ed0b751-0d7d-4342-a818-67910a01df9a'
+ );
+ });
+
describe('can evaluate the attribute value', () => {
let scenario: Scenario;
diff --git a/packages/xforms-engine/src/instance/attachments/buildAttributes.ts b/packages/xforms-engine/src/instance/attachments/buildAttributes.ts
index d62cb4773..e4b353590 100644
--- a/packages/xforms-engine/src/instance/attachments/buildAttributes.ts
+++ b/packages/xforms-engine/src/instance/attachments/buildAttributes.ts
@@ -1,3 +1,6 @@
+import type { StaticAttribute } from '../../integration/xpath/static-dom/StaticAttribute.ts';
+import type { StaticDocument } from '../../integration/xpath/static-dom/StaticDocument.ts';
+import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
import { Attribute } from '../Attribute';
import type { AnyNode } from '../hierarchy.ts';
import type { InputControl } from '../InputControl.ts';
@@ -5,6 +8,19 @@ import type { ModelValue } from '../ModelValue.ts';
import type { Note } from '../Note.ts';
import type { RangeControl } from '../RangeControl.ts';
+function buildInstanceAttributeMap(
+ instanceNode: StaticAttribute | StaticDocument | StaticElement | null
+): Map {
+ const map = new Map();
+ if (!instanceNode) {
+ return map;
+ }
+ for (const attribute of instanceNode.attributes) {
+ map.set(attribute.qualifiedName.getPrefixedName(), attribute);
+ }
+ return map;
+}
+
export function buildAttributes(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
owner: AnyNode | InputControl | ModelValue | Note | RangeControl
@@ -13,7 +29,11 @@ export function buildAttributes(
if (!attributes) {
return [];
}
+ const instanceAttributes = buildInstanceAttributeMap(owner.instanceNode);
return Array.from(attributes.values()).map((attributeDefinition) => {
- return new Attribute(owner, attributeDefinition, attributeDefinition.template);
+ const instanceNode =
+ instanceAttributes.get(attributeDefinition.qualifiedName.getPrefixedName()) ??
+ attributeDefinition.template;
+ return new Attribute(owner, attributeDefinition, instanceNode);
});
}
diff --git a/packages/xforms-engine/src/integration/xpath/static-dom/StaticDocument.ts b/packages/xforms-engine/src/integration/xpath/static-dom/StaticDocument.ts
index 90dc322c9..d11002b91 100644
--- a/packages/xforms-engine/src/integration/xpath/static-dom/StaticDocument.ts
+++ b/packages/xforms-engine/src/integration/xpath/static-dom/StaticDocument.ts
@@ -1,4 +1,5 @@
import type { XFormsXPathDocument } from '../adapter/XFormsXPathNode.ts';
+import type { StaticAttribute } from './StaticAttribute.ts';
import type { StaticElementOptions } from './StaticElement.ts';
import { StaticElement } from './StaticElement.ts';
import { StaticParentNode } from './StaticParentNode.ts';
@@ -15,6 +16,7 @@ export class StaticDocument extends StaticParentNode<'document'> implements XFor
readonly nodeset: string;
readonly children: readonly [root: StaticElement];
readonly childElements: readonly [root: StaticElement];
+ readonly attributes: readonly StaticAttribute[] = [];
constructor(options: StaticDocumentOptions) {
super('document');