Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -175,6 +175,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