Skip to content
Open
8 changes: 8 additions & 0 deletions .changeset/nasty-impalas-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@getodk/xforms-engine': minor
'@getodk/web-forms': minor
'@getodk/scenario': minor
'@getodk/common': minor
---

Add support for all jr:preload options
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ This section is auto generated. Please update `feature-matrix.json` and then run
<summary>

<!-- prettier-ignore -->
##### Question types (basic functionality)<br/>🟩🟩🟩🟩🟩🟩⬜⬜⬜⬜⬜⬜⬜⬜⬜ 41\%
##### Question types (basic functionality)<br/>🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜⬜⬜⬜ 61\%

</summary>
<br/>
Expand Down Expand Up @@ -66,13 +66,13 @@ This section is auto generated. Please update `feature-matrix.json` and then run
| rank | ✅ |
| csv-external | ✅ |
| acknowledge | 🚧 |
| start | |
| end | |
| today | |
| deviceid | |
| username | |
| phonenumber | |
| email | |
| start | |
| end | |
| today | |
| deviceid | |
| username | |
| phonenumber | |
| email | |
| audit | |

</details>
Expand Down
14 changes: 7 additions & 7 deletions feature-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
"rank": "✅",
"csv-external": "✅",
"acknowledge": "🚧",
"start": "",
"end": "",
"today": "",
"deviceid": "",
"username": "",
"phonenumber": "",
"email": "",
"start": "",
"end": "",
"today": "",
"deviceid": "",
"username": "",
"phonenumber": "",
"email": "",
"audit": ""
},
"Appearances": {
Expand Down
56 changes: 56 additions & 0 deletions packages/common/src/fixtures/test-javarosa/resources/preload.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:jr="http://openrosa.org/javarosa">
<h:head>
<h:title>jr:preload</h:title>
<model>
<instance>
<data id="preload" version="1">
<today/>
<start/>
<end/>
<deviceid/>
<phonenumber/>
<email/>
<username/>
<meta>
<instanceID/>
</meta>
</data>
</instance>
<bind jr:preload="date" jr:preloadParams="today" nodeset="/data/today" type="date" readonly="true()"/>
<bind jr:preload="timestamp" jr:preloadParams="start" nodeset="/data/start" type="dateTime" readonly="true()"/>
<bind jr:preload="timestamp" jr:preloadParams="end" nodeset="/data/end" type="dateTime" readonly="true()"/>
<bind jr:preload="property" jr:preloadParams="deviceid" nodeset="/data/deviceid" type="string" readonly="true()"/>
<bind jr:preload="property" jr:preloadParams="phonenumber" nodeset="/data/phonenumber" type="string" readonly="true()"/>
<bind jr:preload="property" jr:preloadParams="email" nodeset="/data/email" type="string" readonly="true()"/>
<bind jr:preload="property" jr:preloadParams="username" nodeset="/data/username" type="string" readonly="true()"/>
<bind jr:preload="uid" nodeset="/data/meta/instanceID" type="string" readonly="true()"/>
</model>
</h:head>
<h:body>
<input ref="/data/today">
<label>today</label>
</input>
<input ref="/data/start">
<label>start</label>
</input>
<input ref="/data/end">
<label>end</label>
</input>
<input ref="/data/deviceid">
<label>deviceid</label>
</input>
<input ref="/data/phonenumber">
<label>phonenumber</label>
</input>
<input ref="/data/email">
<label>email</label>
</input>
<input ref="/data/username">
<label>username</label>
</input>
<input ref="/data/meta/instanceID">
<label>instanceID</label>
</input>
</h:body>
</h:html>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { JAVAROSA_PREFIX } from '../../../constants/xmlns.ts';
import { EmptyXFormsElement } from './EmptyXFormsElement.ts';
import type { XFormsElement } from './XFormsElement.ts';

Expand Down Expand Up @@ -56,9 +57,11 @@ class BindBuilderXFormsElement implements XFormsElement {
}

preload(expression: string): BindBuilderXFormsElement {
this.bindAttributes.set('jr:preload', expression);
return this.withAttribute(JAVAROSA_PREFIX, 'preload', expression);
}

return this;
preloadParams(expression: string): BindBuilderXFormsElement {
return this.withAttribute(JAVAROSA_PREFIX, 'preloadParams', expression);
}

readonly(expression = 'true()'): BindBuilderXFormsElement {
Expand Down
3 changes: 3 additions & 0 deletions packages/scenario/src/client/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
LoadFormWarningResult,
MissingResourceBehavior,
OpaqueReactiveObjectFactory,
PreloadProperties,
RootNode,
} from '@getodk/xforms-engine';
import { createInstance } from '@getodk/xforms-engine';
Expand All @@ -27,6 +28,7 @@ export interface TestFormOptions {
readonly missingResourceBehavior: MissingResourceBehavior;
readonly stateFactory: OpaqueReactiveObjectFactory;
readonly instanceAttachments: InstanceAttachmentsConfig;
readonly preloadProperties: PreloadProperties;
}

const defaultConfig = {
Expand Down Expand Up @@ -62,6 +64,7 @@ export const initializeTestForm = async (
instance: {
stateFactory: options.stateFactory,
instanceAttachments: options.instanceAttachments,
preloadProperties: options.preloadProperties,
},
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/scenario/src/jr/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export class Scenario {
fileNameFactory: ({ basename, extension }) => `${basename}${extension ?? ''}`,
...overrideOptions?.instanceAttachments,
},
preloadProperties: overrideOptions?.preloadProperties ?? {},
};
}

Expand Down
204 changes: 173 additions & 31 deletions packages/scenario/test/jr-preload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,187 @@ import {
t,
title,
} from '@getodk/common/test/fixtures/xform-dsl/index.ts';
import { Temporal } from 'temporal-polyfill';
import { describe, expect, it } from 'vitest';
import { Scenario } from '../src/jr/Scenario.ts';

const CENTRAL_DATE_FORMAT_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
const CENTRAL_DATETIME_FORMAT_REGEX =
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[+|-][0-9]{2}:[0-9]{2}$/;

describe('`jr:preload`', () => {
// ported from: https://github.com/getodk/javarosa/blob/2dd8e15e9f3110a86f8d7d851efc98627ae5692e/src/test/java/org/javarosa/core/model/utils/test/QuestionPreloaderTest.java#L23
it('preloads specified data in bound elements', async () => {
const scenario = await Scenario.init(
'Preload attribute',
html(
head(
title('Preload element'),
model(
mainInstance(t('data id="preload-attribute"', t('element'))),
bind('/data/element').preload('uid')
)
),
body(input('/data/element'))
)
);
describe('uid', () => {
// ported from: https://github.com/getodk/javarosa/blob/2dd8e15e9f3110a86f8d7d851efc98627ae5692e/src/test/java/org/javarosa/core/model/utils/test/QuestionPreloaderTest.java#L23
it('preloads specified data in bound elements', async () => {
const scenario = await Scenario.init(
'Preload attribute',
html(
head(
title('Preload element'),
model(
mainInstance(t('data id="preload-attribute"', t('element'))),
bind('/data/element').preload('uid')
)
),
body(input('/data/element'))
)
);

expect(scenario.answerOf('/data/element')).toStartWith('uuid:');
});

// ported from: https://github.com/getodk/javarosa/blob/2dd8e15e9f3110a86f8d7d851efc98627ae5692e/src/test/java/org/javarosa/core/model/utils/test/QuestionPreloaderTest.java#L43
it('preloads specified data in bound attributes', async () => {
const scenario = await Scenario.init(
'Preload attribute',
html(
head(
title('Preload attribute'),
model(
mainInstance(t('data id="preload-attribute"', t('element attr=""'))),
bind('/data/element/@attr').preload('uid')
)
),
body(input('/data/element'))
)
);

expect(scenario.attributeOf('/data/element', 'attr')).toStartWith('uuid:');
});
});

describe('datetime', () => {
it('preloads timestamp start', async () => {
const start = Temporal.Now.instant().epochNanoseconds;
const scenario = await Scenario.init(
'Preload start date',
html(
head(
title('Preload start date'),
model(
mainInstance(t('data id="preload-attribute"', t('element'))),
bind('/data/element').type('xsd:dateTime').preload('timestamp').preloadParams('start')
)
),
body()
)
);
const end = Temporal.Now.instant().epochNanoseconds;
const val = scenario.answerOf('/data/element').toString();
const actual = Temporal.Instant.from(val).epochNanoseconds;

expect(actual).toBeGreaterThanOrEqual(start);
expect(actual).toBeLessThanOrEqual(end);

expect(scenario.answerOf('/data/element')).toStartWith('uuid:');
expect(val).toMatch(CENTRAL_DATETIME_FORMAT_REGEX);
});

it('preloads date today', async () => {
const start = Temporal.Now.plainDateISO();
const scenario = await Scenario.init(
'Preload start date',
html(
head(
title('Preload start date'),
model(
mainInstance(t('data id="preload-attribute"', t('element'))),
bind('/data/element').type('xsd:date').preload('date').preloadParams('today')
)
),
body()
)
);
const end = Temporal.Now.plainDateISO();

expect(scenario.answerOf('/data/element').toString()).toSatisfy((actual: string) => {
const actualDate = Temporal.PlainDate.from(actual);
expect(actual).toMatch(CENTRAL_DATE_FORMAT_REGEX);
return actualDate.equals(start) || actualDate.equals(end); // just in case this test runs at midnight...
});
});

it('preloads timestamp end', async () => {
const scenario = await Scenario.init(
'Preload end date',
html(
head(
title('Preload end date'),
model(
mainInstance(t('data id="preload-attribute"', t('element'))),
bind('/data/element').type('xsd:dateTime').preload('timestamp').preloadParams('end')
)
),
body()
)
);
expect(scenario.answerOf('/data/element').toString()).toEqual(''); // doesn't trigger until submission

const start = Temporal.Now.instant().epochNanoseconds;
await scenario.prepareWebFormsInstancePayload();
const xml = scenario.proposed_serializeInstance();
const end = Temporal.Now.instant().epochNanoseconds;
const timestampElement = /<element>(.*)<\/element>/g.exec(xml);
if (!timestampElement || timestampElement.length < 2 || !timestampElement[1]) {
throw new Error('element not found');
}

const val = timestampElement[1];

const actual = Temporal.Instant.from(val).epochNanoseconds;
expect(actual).toBeGreaterThanOrEqual(start);
expect(actual).toBeLessThanOrEqual(end);

expect(val).toMatch(CENTRAL_DATETIME_FORMAT_REGEX);
});
});

// ported from: https://github.com/getodk/javarosa/blob/2dd8e15e9f3110a86f8d7d851efc98627ae5692e/src/test/java/org/javarosa/core/model/utils/test/QuestionPreloaderTest.java#L43
it('preloads specified data in bound attributes', async () => {
const scenario = await Scenario.init(
'Preload attribute',
html(
head(
title('Preload attribute'),
model(
mainInstance(t('data id="preload-attribute"', t('element attr=""'))),
bind('/data/element/@attr').preload('uid')
)
describe('property', () => {
it('bound from given properties', async () => {
const deviceID = '123456';
const email = 'my@email';
const username = 'mukesh';
const phoneNumber = '+15551234';

const scenario = await Scenario.init(
'Properties',
html(
head(
title('Properties'),
model(
mainInstance(
t(
'data id="properties"',
t('deviceid'),
t('email'),
t('username'),
t('phonenumber')
)
),
bind('/data/deviceid').type('string').preload('property').preloadParams('deviceid'),
bind('/data/email').type('string').preload('property').preloadParams('email'),
bind('/data/username').type('string').preload('property').preloadParams('username'),
bind('/data/phonenumber')
.type('string')
.preload('property')
.preloadParams('phonenumber')
)
),
body()
),
body(input('/data/element'))
)
);
{
preloadProperties: {
deviceID,
email,
username,
phoneNumber,
},
}
);

expect(scenario.attributeOf('/data/element', 'attr')).toStartWith('uuid:');
expect(scenario.answerOf('/data/deviceid').toString()).to.equal(deviceID);
expect(scenario.answerOf('/data/email').toString()).to.equal(email);
expect(scenario.answerOf('/data/username').toString()).to.equal(username);
expect(scenario.answerOf('/data/phonenumber').toString()).to.equal(phoneNumber);
});
});
});
Loading