diff --git a/packages/scenario/src/error/UnclearApplicabilityError.ts b/packages/scenario/src/error/UnclearApplicabilityError.ts new file mode 100644 index 00000000..0c8750e3 --- /dev/null +++ b/packages/scenario/src/error/UnclearApplicabilityError.ts @@ -0,0 +1,7 @@ +export class UnclearApplicabilityError extends Error { + constructor(subject: string) { + super( + `It is not yet clear if/how ${subject} functionality will be applicable in the web forms project` + ); + } +} diff --git a/packages/scenario/src/jr/Scenario.ts b/packages/scenario/src/jr/Scenario.ts index 03afbc86..7157a3e4 100644 --- a/packages/scenario/src/jr/Scenario.ts +++ b/packages/scenario/src/jr/Scenario.ts @@ -1,5 +1,5 @@ import type { XFormsElement } from '@odk-web-forms/common/test/fixtures/xform-dsl/XFormsElement.ts'; -import type { RootNode } from '@odk-web-forms/xforms-engine'; +import type { AnyNode, RootNode } from '@odk-web-forms/xforms-engine'; import type { Accessor, Setter } from 'solid-js'; import { createMemo, createSignal, runWithOwner } from 'solid-js'; import { afterEach, expect } from 'vitest'; @@ -8,6 +8,7 @@ import { answerOf } from '../client/answerOf.ts'; import type { TestFormResource } from '../client/init.ts'; import { initializeTestForm } from '../client/init.ts'; import { getClosestRepeatRange } from '../client/traversal.ts'; +import { UnclearApplicabilityError } from '../error/UnclearApplicabilityError.ts'; import { PositionalEvent } from './event/PositionalEvent.ts'; import { RepeatInstanceEvent } from './event/RepeatInstanceEvent.ts'; import { @@ -16,6 +17,7 @@ import { type NonTerminalPositionalEvent, type PositionalEvents, } from './event/getPositionalEvents.ts'; +import { TreeReference } from './instance/TreeReference.ts'; import type { PathResource } from './resource/PathResource.ts'; import { SelectChoiceList } from './select/SelectChoiceList.ts'; @@ -291,4 +293,26 @@ export class Scenario { this.instanceRoot.setLanguage(language); } + + refAtIndex(): TreeReference { + const event = this.getSelectedPositionalEvent(); + + let treeReferenceNode: AnyNode; + + if (event.eventType === 'END_OF_FORM') { + treeReferenceNode = this.instanceRoot; + } else { + treeReferenceNode = event.node; + } + + return new TreeReference(treeReferenceNode); + } + + /** + * @todo it is not clear if/how we'll use similar logic in web forms. It + * seems most likely to be applicable to offline capabilities. + */ + serializeAndDeserializeForm(): Promise { + return Promise.reject(new UnclearApplicabilityError('serialization/deserialization')); + } } diff --git a/packages/scenario/src/jr/instance/TreeReference.ts b/packages/scenario/src/jr/instance/TreeReference.ts new file mode 100644 index 00000000..a58c2ade --- /dev/null +++ b/packages/scenario/src/jr/instance/TreeReference.ts @@ -0,0 +1,21 @@ +import type { AnyNode } from '@odk-web-forms/xforms-engine'; + +/** + * Wraps a node (as in `@odk-web-forms/xforms-engine` semantic terms) to provide + * interface compatibility with JavaRosa's `TreeReference`, as minimally as + * possible to satisfy ported test logic/assertions. + */ +export class TreeReference { + constructor(readonly node: AnyNode) {} + + /** + * @todo Currently the engine doesn't provide a representation of secondary + * instances **at all**. This seems "correct" for the current engine/client + * responsibility boundaries, but that may change if/as we expand the + * client concept into other areas of functionality beyond the primary form- + * filling UI use cases. + */ + getInstanceName(): string | null { + return null; + } +} diff --git a/packages/scenario/src/value/ExpectedNullValue.ts b/packages/scenario/src/value/ExpectedNullValue.ts new file mode 100644 index 00000000..1dcb48f9 --- /dev/null +++ b/packages/scenario/src/value/ExpectedNullValue.ts @@ -0,0 +1,14 @@ +/** + * @todo Before bailing on the port of `FormDefSerializationTest.java`, it + * seemed quite likely there should be a `ComparableValue` abstraction below + * `ComparableAnswer`: `null` is almost certainly not an "answer", but we might + * find that there is a more general sense of comparable **and castable** + * assertion values, of which "answer" may be a more specific case. + */ +export class ExpectedNullValue { + constructor() { + throw new Error('TODO: see `ExpectedNullValue` JSDoc'); + } +} + +export const nullValue = () => null; diff --git a/packages/scenario/test/serialization.test.ts b/packages/scenario/test/serialization.test.ts new file mode 100644 index 00000000..a11c379f --- /dev/null +++ b/packages/scenario/test/serialization.test.ts @@ -0,0 +1,76 @@ +import { + bind, + body, + head, + html, + input, + mainInstance, + model, + t, + title, +} from '@odk-web-forms/common/test/fixtures/xform-dsl/index.ts'; +import { describe, expect, it } from 'vitest'; +import { Scenario } from '../src/jr/Scenario.ts'; +import { nullValue } from '../src/value/ExpectedNullValue.ts'; + +// Ported as of https://github.com/getodk/javarosa/commit/5ae68946c47419b83e7d28290132d846e457eea6 +describe('Serialization', () => { + /** + * **PORTING NOTES** + * + * Discussing these tests in Slack, it was determined that they're not + * currently applicable to the web forms project. The first test is ported + * largely because it was already most of the way there by this point, + * although much of the supporting logic for a more faithful port has been + * omitted for now (with some breadcrumbs on thinking behind that supporting + * logic left in for posterity in case it becomes applicable in the future). + * + * We reached the conclusion to defer porting the rest of these tests for now, + * on the basis that the current serialization functionality under test is in + * support of specific performance optimizations; that the tests exercise + * implementation details not directly pertinent to web forms at this time; + * that the performance optimizations in question are themselves not likely to + * be pertinent to web forms, at least at this time. + * + * We also agreed that this note would serve as an explanation of the above, + * as well as an opportunity to briefly mention that we may have other reasons + * to support (de)serialization, and to test that support, likely around + * offline functionality. As such, we may want to revisit these skipped + * JavaRosa tests if they seem valuable for that effort when we get to it. + */ + describe.skip('FormDefSerializationTest.java', () => { + const getSimplestFormScenario = async (): Promise => { + return Scenario.init( + 'Simplest', + html( + head( + title('Simplest'), + model(mainInstance(t('data id="simplest"', t('a'))), bind('/data/a').type('string')) + ), + body(input('/data/a')) + ) + ); + }; + + describe('instance name', () => { + describe('for reference in main instance', () => { + it.skip('is always null', async () => { + const scenario = await getSimplestFormScenario(); + + scenario.next('/data/a'); + + expect(scenario.refAtIndex().getInstanceName()).toEqual(nullValue()); + + const deserialized = await scenario.serializeAndDeserializeForm(); + + deserialized.next('/data/a'); + + expect(deserialized.refAtIndex().getInstanceName()).toEqual(nullValue()); + }); + + it.skip('instanceName_forFormDefEvaluationContext_isAlwaysNull'); + it.skip('instanceName_forFormDefMainInstance_isAlwaysNull'); + }); + }); + }); +});