diff --git a/.changeset/new-dolls-dress.md b/.changeset/new-dolls-dress.md new file mode 100644 index 000000000..fc719ef27 --- /dev/null +++ b/.changeset/new-dolls-dress.md @@ -0,0 +1,5 @@ +--- +'@getodk/xforms-engine': minor +--- + +**BREAKING CHANGE** rename instance payload property `definition` to `SubmissionMeta` diff --git a/.changeset/pretty-grapes-applaud.md b/.changeset/pretty-grapes-applaud.md new file mode 100644 index 000000000..7d32721cb --- /dev/null +++ b/.changeset/pretty-grapes-applaud.md @@ -0,0 +1,5 @@ +--- +'@getodk/xforms-engine': minor +--- + +**BREAKING CHANGE** instance payload data is always produced as a tuple. The shape for a "chunked" payload is unchanged. The shape for a "monolithic" payload is now more like a "chunked" payload, except that it is guaranteed to always have only one item. diff --git a/packages/scenario/src/assertion/extensions/submission.ts b/packages/scenario/src/assertion/extensions/submission.ts index 866a06dda..03aff8cfa 100644 --- a/packages/scenario/src/assertion/extensions/submission.ts +++ b/packages/scenario/src/assertion/extensions/submission.ts @@ -11,12 +11,7 @@ import { } from '@getodk/common/test/assertions/helpers.ts'; import type { SimpleAssertionResult } from '@getodk/common/test/assertions/vitest/shared-extension-types.ts'; import type { AssertIs } from '@getodk/common/types/assertions/AssertIs.ts'; -import type { - SubmissionChunkedType, - SubmissionData, - SubmissionInstanceFile, - SubmissionResult, -} from '@getodk/xforms-engine'; +import type { InstanceFile, InstancePayload, InstancePayloadType } from '@getodk/xforms-engine'; import { constants } from '@getodk/xforms-engine'; import { assert, expect } from 'vitest'; import { Scenario } from '../../jr/Scenario.ts'; @@ -38,10 +33,10 @@ const compareSubmissionXML = (actual: string, expected: string): SimpleAssertion const assertFormData: AssertIs = instanceAssertion(FormData); -type AnySubmissionResult = SubmissionResult; +type AnyInstancePayload = InstancePayload; /** - * Validating the full {@link SubmissionResult} type is fairly involved. We + * Validating the full {@link InstancePayload} type is fairly involved. We * check the basic object shape (expected keys present, gut check a few easy to * check property types), on the assumption that downstream assertions will fail * if the runtime and static types disagree. @@ -50,13 +45,13 @@ type AnySubmissionResult = SubmissionResult; * more complete validation here, serving as a smoke test for all tests * exercising aspects of a prepared submission result. */ -const assertSubmissionResult: AssertIs = (value) => { +const assertInstancePayload: AssertIs = (value) => { assertUnknownObject(value); assertString(value.status); if (value.violations !== null) { assertUnknownArray(value.violations); } - assertUnknownObject(value.definition); + assertUnknownObject(value.submissionMeta); if (Array.isArray(value.data)) { value.data.forEach((item) => { @@ -69,47 +64,25 @@ const assertSubmissionResult: AssertIs = (value) => { const assertFile: AssertIs = instanceAssertion(File); -const { SUBMISSION_INSTANCE_FILE_NAME, SUBMISSION_INSTANCE_FILE_TYPE } = constants; +const { INSTANCE_FILE_NAME, INSTANCE_FILE_TYPE } = constants; -const assertSubmissionInstanceFile: AssertIs = (value) => { +const assertInstanceFile: AssertIs = (value) => { assertFile(value); - if (value.name !== SUBMISSION_INSTANCE_FILE_NAME) { - throw new Error(`Expected file named ${SUBMISSION_INSTANCE_FILE_NAME}, got ${value.name}`); + if (value.name !== INSTANCE_FILE_NAME) { + throw new Error(`Expected file named ${INSTANCE_FILE_NAME}, got ${value.name}`); } - if (value.type !== SUBMISSION_INSTANCE_FILE_TYPE) { - throw new Error(`Expected file of type ${SUBMISSION_INSTANCE_FILE_TYPE}, got ${value.type}`); + if (value.type !== INSTANCE_FILE_TYPE) { + throw new Error(`Expected file of type ${INSTANCE_FILE_TYPE}, got ${value.type}`); } }; -type ChunkedSubmissionData = readonly [SubmissionData, ...SubmissionData[]]; +const getInstanceFile = (payload: AnyInstancePayload): InstanceFile => { + const [instanceData] = payload.data; + const file = instanceData.get(INSTANCE_FILE_NAME); -const isChunkedSubmissionData = ( - data: ChunkedSubmissionData | SubmissionData -): data is ChunkedSubmissionData => { - return Array.isArray(data); -}; - -const getSubmissionData = (submissionResult: AnySubmissionResult): SubmissionData => { - const { data } = submissionResult; - - if (isChunkedSubmissionData(data)) { - const [first] = data; - - return first; - } - - return data; -}; - -const getSubmissionInstanceFile = ( - submissionResult: AnySubmissionResult -): SubmissionInstanceFile => { - const submissionData = getSubmissionData(submissionResult); - const file = submissionData.get(SUBMISSION_INSTANCE_FILE_NAME); - - assertSubmissionInstanceFile(file); + assertInstanceFile(file); return file; }; @@ -125,30 +98,27 @@ export const submissionExtensions = extendExpect(expect, { } ), - toBeReadyForSubmission: new ArbitraryConditionExpectExtension( - assertSubmissionResult, - (result) => { - try { - expect(result).toMatchObject({ - status: 'ready', - violations: null, - }); - - return true; - } catch (error) { - if (error instanceof Error) { - return error; - } - - // eslint-disable-next-line no-console - console.error(error); - return new Error('Unknown error'); + toBeReadyForSubmission: new ArbitraryConditionExpectExtension(assertInstancePayload, (result) => { + try { + expect(result).toMatchObject({ + status: 'ready', + violations: null, + }); + + return true; + } catch (error) { + if (error instanceof Error) { + return error; } + + // eslint-disable-next-line no-console + console.error(error); + return new Error('Unknown error'); } - ), + }), toBePendingSubmissionWithViolations: new ArbitraryConditionExpectExtension( - assertSubmissionResult, + assertInstancePayload, (result) => { try { expect(result.status).toBe('pending'); @@ -173,10 +143,10 @@ export const submissionExtensions = extendExpect(expect, { ), toHavePreparedSubmissionXML: new AsyncAsymmetricTypedExpectExtension( - assertSubmissionResult, + assertInstancePayload, assertString, async (actual, expected): Promise => { - const instanceFile = getSubmissionInstanceFile(actual); + const instanceFile = getInstanceFile(actual); const actualText = await getBlobText(instanceFile); return compareSubmissionXML(actualText, expected); diff --git a/packages/scenario/src/jr/Scenario.ts b/packages/scenario/src/jr/Scenario.ts index 4632c71c6..bbaef6cdb 100644 --- a/packages/scenario/src/jr/Scenario.ts +++ b/packages/scenario/src/jr/Scenario.ts @@ -12,8 +12,8 @@ import { constants as ENGINE_CONSTANTS } from '@getodk/xforms-engine'; import type { Accessor, Setter } from 'solid-js'; import { createMemo, createSignal, runWithOwner } from 'solid-js'; import { afterEach, assert, expect } from 'vitest'; -import { SelectValuesAnswer } from '../answer/SelectValuesAnswer.ts'; import { RankValuesAnswer } from '../answer/RankValuesAnswer.ts'; +import { SelectValuesAnswer } from '../answer/SelectValuesAnswer.ts'; import type { ValueNodeAnswer } from '../answer/ValueNodeAnswer.ts'; import { answerOf } from '../client/answerOf.ts'; import type { InitializeTestFormOptions, TestFormResource } from '../client/init.ts'; @@ -964,7 +964,7 @@ export class Scenario { } proposed_serializeInstance(): string { - return this.instanceRoot.submissionState.submissionXML; + return this.instanceRoot.instanceState.instanceXML; } /** @@ -974,8 +974,8 @@ export class Scenario { * more about Collect's responsibility for submission (beyond serialization, * already handled by {@link proposed_serializeInstance}). */ - prepareWebFormsSubmission() { - return this.instanceRoot.prepareSubmission(); + prepareWebFormsInstancePayload() { + return this.instanceRoot.prepareInstancePayload(); } // TODO: consider adapting tests which use the following interfaces to use diff --git a/packages/scenario/test/submission.test.ts b/packages/scenario/test/submission.test.ts index 0667b3962..df32aa7cb 100644 --- a/packages/scenario/test/submission.test.ts +++ b/packages/scenario/test/submission.test.ts @@ -750,26 +750,26 @@ describe('Form submission', () => { return scenario; }; - describe('submission definition', () => { - it('includes a default submission definition', async () => { + describe('submission meta', () => { + it('includes default submission meta', async () => { const scenario = await buildSubmissionPayloadScenario(); - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); - expect(submissionResult.definition).toMatchObject({ + expect(submissionResult.submissionMeta).toMatchObject({ submissionAction: null, submissionMethod: 'post', encryptionKey: null, }); }); - it('includes a form-specified submission definition URL', async () => { + it('includes a form-specified submission URL', async () => { const submissionAction = 'https://example.org'; const scenario = await buildSubmissionPayloadScenario({ submissionElements: [t(`submission action="${submissionAction}"`)], }); - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); - expect(submissionResult.definition).toMatchObject({ + expect(submissionResult.submissionMeta).toMatchObject({ submissionAction: new URL(submissionAction), }); }); @@ -778,9 +778,9 @@ describe('Form submission', () => { const scenario = await buildSubmissionPayloadScenario({ submissionElements: [t('submission method="post"')], }); - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); - expect(submissionResult.definition).toMatchObject({ + expect(submissionResult.submissionMeta).toMatchObject({ submissionMethod: 'post', }); }); @@ -789,9 +789,9 @@ describe('Form submission', () => { const scenario = await buildSubmissionPayloadScenario({ submissionElements: [t('submission method="form-data-post"')], }); - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); - expect(submissionResult.definition).toMatchObject({ + expect(submissionResult.submissionMeta).toMatchObject({ submissionMethod: 'post', }); }); @@ -823,9 +823,9 @@ describe('Form submission', () => { ), ], }); - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); - expect(submissionResult.definition).toMatchObject({ + expect(submissionResult.submissionMeta).toMatchObject({ encryptionKey: base64RsaPublicKey, }); }); @@ -858,13 +858,13 @@ describe('Form submission', () => { }); it('is ready for submission when instance state is valid', async () => { - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); expect(submissionResult).toBeReadyForSubmission(); }); it('includes submission instance XML file data', async () => { - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); await expect(submissionResult).toHavePreparedSubmissionXML(validSubmissionXML); }); @@ -895,13 +895,13 @@ describe('Form submission', () => { }); it('is pending submission with violations', async () => { - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); expect(submissionResult).toBePendingSubmissionWithViolations(); }); it('produces submission instance XML file data even when current instance state is invalid', async () => { - const submissionResult = await scenario.prepareWebFormsSubmission(); + const submissionResult = await scenario.prepareWebFormsInstancePayload(); await expect(submissionResult).toHavePreparedSubmissionXML(invalidSubmissionXML); }); diff --git a/packages/web-forms/src/components/OdkWebForm.vue b/packages/web-forms/src/components/OdkWebForm.vue index e06d01d2a..feed0376a 100644 --- a/packages/web-forms/src/components/OdkWebForm.vue +++ b/packages/web-forms/src/components/OdkWebForm.vue @@ -1,8 +1,8 @@