Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial engine support for submissions #226

Merged
merged 18 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
682e9c2
xforms-engine: reorganize a couple of parse test suites missed in #213
eyelidlessness Sep 13, 2024
cd64257
Initial engine/client interface: submission
eyelidlessness Aug 30, 2024
4021e17
escapeXMLText
eyelidlessness Sep 23, 2024
97473ce
Engine/client: introduce intermediate XML-only serialized submission …
eyelidlessness Sep 23, 2024
67678a2
Implement submission XML serialization; tests pending
eyelidlessness Sep 25, 2024
6e84a47
ui-solid: incorporate submission XML serialization
eyelidlessness Sep 23, 2024
dfd7ae3
scenario: ComparableAssertableValue (which ComparableAnswer now extends)
eyelidlessness Sep 25, 2024
ca9c779
scenario: integrate submission XML serialization support for testing
eyelidlessness Sep 27, 2024
e856c9f
scenario: update existing submission XML serialization test, now passing
eyelidlessness Sep 27, 2024
4435cbd
scenario: expand submission test suite
eyelidlessness Sep 27, 2024
93a25b2
scenario: add support for testing client reactivity (Solid)
eyelidlessness Sep 25, 2024
e86a52a
scenario: test client reactivity of submission serialization
eyelidlessness Sep 27, 2024
4c48949
engine: parse <submission>, implementing SubmissionDefinition
eyelidlessness Sep 26, 2024
e7a511f
scenario: add method aliased to RootNode `prepareSubmission`
eyelidlessness Sep 26, 2024
246b78c
scenario: assertion extensions for aspects of `prepareSubmission`/`Su…
eyelidlessness Sep 26, 2024
b3c193f
scenario: test prepared single-request submission (`SubmissionResult<…
eyelidlessness Sep 26, 2024
832e762
engine: implementation of applicable aspects of `prepareSubmission`
eyelidlessness Sep 26, 2024
8edf375
Changeset
eyelidlessness Sep 27, 2024
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/yellow-tomatoes-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@getodk/xforms-engine': minor
'@getodk/scenario': minor
'@getodk/ui-solid': patch
'@getodk/common': patch
---

Initial engine support for preparing submissions
7 changes: 7 additions & 0 deletions packages/common/src/lib/type-assertions/assertNull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type AssertNull = (value: unknown) => asserts value is null;

export const assertNull: AssertNull = (value) => {
if (value !== null) {
throw new Error('Not null');
}
};
9 changes: 9 additions & 0 deletions packages/common/src/lib/type-assertions/assertUnknownArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type UnknownArray = readonly unknown[];

type AssertUnknownArray = (value: unknown) => asserts value is UnknownArray;

export const assertUnknownArray: AssertUnknownArray = (value) => {
if (!Array.isArray(value)) {
throw new Error('Not an array');
}
};
8 changes: 5 additions & 3 deletions packages/common/src/test/assertions/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ export { instanceAssertion } from './instanceAssertion.ts';
export { typeofAssertion } from './typeofAssertion.ts';
export { ArbitraryConditionExpectExtension } from './vitest/ArbitraryConditionExpectExtension.ts';
export { AsymmetricTypedExpectExtension } from './vitest/AsymmetricTypedExpectExtension.ts';
export { InspectableComparisonError } from './vitest/InspectableComparisonError.ts';
export { StaticConditionExpectExtension } from './vitest/StaticConditionExpectExtension.ts';
export { SymmetricTypedExpectExtension } from './vitest/SymmetricTypedExpectExtension.ts';
export { AsyncAsymmetricTypedExpectExtension } from './vitest/AsyncAsymmetricTypedExpectExtension.ts';
export { extendExpect } from './vitest/extendExpect.ts';
export { InspectableComparisonError } from './vitest/InspectableComparisonError.ts';
export { InspectableStaticConditionError } from './vitest/InspectableStaticConditionError.ts';
export type {
CustomInspectable,
DeriveStaticVitestExpectExtension,
Inspectable,
} from './vitest/shared-extension-types.ts';
export { StaticConditionExpectExtension } from './vitest/StaticConditionExpectExtension.ts';
export { SymmetricTypedExpectExtension } from './vitest/SymmetricTypedExpectExtension.ts';
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { SyncExpectationResult } from 'vitest';
import type { AssertIs } from '../../../../types/assertions/AssertIs.ts';
import { expandAsyncExpectExtensionResult } from './expandAsyncExpectExtensionResult.ts';
import type { ExpectExtensionMethod, SimpleAssertionResult } from './shared-extension-types.ts';
import { validatedExtensionMethod } from './validatedExtensionMethod.ts';

/**
* Generalizes definition of a Vitest `expect` API extension where the assertion
* expects differing types for its `actual` and `expected` parameters, and:
*
* - Automatically perfoms runtime validation of those parameters, helping to
* ensure that the extensions' static types are consistent with the runtime
* values passed in a given test's assertions
*
* - Expands simplified assertion result types to the full interface expected by
* Vitest
*
* - Facilitates deriving and defining corresponding static types on the base
* `expect` type
*/
export class AsyncAsymmetricTypedExpectExtension<
Actual = unknown,
Expected = Actual,
Result extends SimpleAssertionResult = SimpleAssertionResult,
> {
readonly extensionMethod: ExpectExtensionMethod<unknown, unknown, Promise<SyncExpectationResult>>;

constructor(
readonly validateActualArgument: AssertIs<Actual>,
readonly validateExpectedArgument: AssertIs<Expected>,
extensionMethod: ExpectExtensionMethod<Actual, Expected, Promise<Result>>
) {
const validatedMethod = validatedExtensionMethod(
validateActualArgument,
validateExpectedArgument,
extensionMethod
);

this.extensionMethod = expandAsyncExpectExtensionResult(validatedMethod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { SyncExpectationResult } from 'vitest';
import type { expandSimpleExpectExtensionResult } from './expandSimpleExpectExtensionResult.ts';
import { isErrorLike } from './isErrorLike.ts';
import type { ExpectExtensionMethod, SimpleAssertionResult } from './shared-extension-types.ts';

/**
* Asynchronous counterpart to {@link expandSimpleExpectExtensionResult}
*/
export const expandAsyncExpectExtensionResult = <Actual, Expected>(
simpleMethod: ExpectExtensionMethod<Actual, Expected, Promise<SimpleAssertionResult>>
): ExpectExtensionMethod<Actual, Expected, Promise<SyncExpectationResult>> => {
return async (actual, expected) => {
const simpleResult = await simpleMethod(actual, expected);

const pass = simpleResult === true;

if (pass) {
return {
pass,
/**
* @todo It was previously assumed that it would never occur that an
* assertion would pass, and that Vitest would then produce a message
* for that. In hindsight, it makes sense that this case occurs in
* negated assertions (e.g.
* `expect(...).not.toPassSomeCustomAssertion`). It seems
* {@link SimpleAssertionResult} is not a good way to model the
* generalization, and that we may want a more uniform `AssertionResult`
* type which always includes both `pass` and `message` capabilities.
* This is should probably be addressed before we merge the big JR port
* PR, but is being temporarily put aside to focus on porting tests in
* bulk in anticipation of a scope change/hopefully-temporary
* interruption of momentum.
*/
message: () => {
throw new Error('Unsupported `SimpleAssertionResult` runtime value');
},
};
}

let message: () => string;

if (isErrorLike(simpleResult)) {
message = () => simpleResult.message;
} else {
message = () => simpleResult;
}

return {
pass,
message,
};
};
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import type { SyncExpectationResult } from 'vitest';
import type {
ErrorLike,
ExpectExtensionMethod,
SimpleAssertionResult,
} from './shared-extension-types.ts';

const isErrorLike = (result: SimpleAssertionResult): result is ErrorLike => {
return typeof result === 'object' && typeof result.message === 'string';
};
import { isErrorLike } from './isErrorLike.ts';
import type { ExpectExtensionMethod, SimpleAssertionResult } from './shared-extension-types.ts';

/**
* Where Vitest assertion extends may be defined to return a
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/test/assertions/vitest/isErrorLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ErrorLike, SimpleAssertionResult } from './shared-extension-types.ts';

export const isErrorLike = (result: SimpleAssertionResult): result is ErrorLike => {
return typeof result === 'object' && typeof result.message === 'string';
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { JSONValue } from '../../../../types/JSONValue.ts';
import type { Primitive } from '../../../../types/Primitive.ts';
import type { ArbitraryConditionExpectExtension } from './ArbitraryConditionExpectExtension.ts';
import type { AsymmetricTypedExpectExtension } from './AsymmetricTypedExpectExtension.ts';
import type { AsyncAsymmetricTypedExpectExtension } from './AsyncAsymmetricTypedExpectExtension.ts';
import type { StaticConditionExpectExtension } from './StaticConditionExpectExtension.ts';
import type { SymmetricTypedExpectExtension } from './SymmetricTypedExpectExtension.ts';

Expand Down Expand Up @@ -37,14 +38,26 @@ export type ExpectExtensionMethod<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TypedExpectExtension<Actual = any, Expected = Actual> =
| AsymmetricTypedExpectExtension<Actual, Expected>
| AsyncAsymmetricTypedExpectExtension<Actual, Expected>
| SymmetricTypedExpectExtension<Expected>;

export type UntypedExpectExtensionFunction = ExpectExtensionMethod<
type AsyncUntypedExpectExtensionFunction = ExpectExtensionMethod<
unknown,
unknown,
Promise<SyncExpectationResult>
>;

type SyncUntypedExpectExtensionFunction = ExpectExtensionMethod<
unknown,
unknown,
SyncExpectationResult
>;

// prettier-ignore
export type UntypedExpectExtensionFunction =
| AsyncUntypedExpectExtensionFunction
| SyncUntypedExpectExtensionFunction;

export interface UntypedExpectExtensionObject {
readonly extensionMethod: UntypedExpectExtensionFunction;
}
Expand All @@ -54,7 +67,10 @@ export type UntypedExpectExtension =
| UntypedExpectExtensionFunction
| UntypedExpectExtensionObject;

export type ExpectExtension = TypedExpectExtension | UntypedExpectExtension;
// prettier-ignore
export type ExpectExtension =
| TypedExpectExtension
| UntypedExpectExtension;

export type ExpectExtensionRecord<MethodName extends string> = {
[K in MethodName]: ExpectExtension;
Expand All @@ -68,7 +84,10 @@ export type DeriveStaticVitestExpectExtension<
> = {
[K in keyof Implementation]:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Implementation[K] extends ArbitraryConditionExpectExtension<any>
Implementation[K] extends AsyncAsymmetricTypedExpectExtension<any, infer Expected>
? (expected: Expected) => Promise<VitestParameterizedReturn>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
: Implementation[K] extends ArbitraryConditionExpectExtension<any>
? () => VitestParameterizedReturn
// eslint-disable-next-line @typescript-eslint/no-explicit-any
: Implementation[K] extends StaticConditionExpectExtension<any, any>
Expand Down
42 changes: 2 additions & 40 deletions packages/scenario/src/answer/ComparableAnswer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import type { SimpleAssertionResult } from '@getodk/common/test/assertions/vitest/shared-extension-types.ts';
import type { JSONValue } from '@getodk/common/types/JSONValue.ts';
import { ComparableAssertableValue } from '../comparable/ComparableAssertableValue.ts';
import type { Scenario } from '../jr/Scenario.ts';

interface OptionalBooleanComparable {
// Expressed here so it can be overridden as either a `readonly` property or
// as a `get` accessor
readonly booleanValue?: boolean;
}

/**
* Provides a common interface for comparing "answer" values of arbitrary data
* types, where the answer may be obtained from:
Expand All @@ -21,36 +14,5 @@ interface OptionalBooleanComparable {
* {@link https://vitest.dev/guide/extending-matchers.html | extended}
* assertions/matchers.
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -- see OptionalBooleanComparable.booleanValue
export abstract class ComparableAnswer implements OptionalBooleanComparable {
abstract get stringValue(): string;

// To be overridden
equals(
// @ts-expect-error -- part of the interface to be overridden
// eslint-disable-next-line @typescript-eslint/no-unused-vars
answer: ComparableAnswer
): SimpleAssertionResult | null {
return null;
}

/**
* Note: we currently return {@link stringValue} here, but this probably
* won't last as we expand support for other data types. This is why the
* return type is currently `unknown`.
*/
getValue(): unknown {
return this.stringValue;
}

inspectValue(): JSONValue {
return this.stringValue;
}

toString(): string {
return this.stringValue;
}
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -- see OptionalBooleanComparable.booleanValue
export interface ComparableAnswer extends OptionalBooleanComparable {}
export abstract class ComparableAnswer extends ComparableAssertableValue {}
Loading
Loading