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

Port JavaRosa Scenario test suites #110

Merged
merged 140 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
573b1f9
Scenario: prepare for possibility of also porting benchmarks
eyelidlessness Apr 12, 2024
0637af5
Scenario: init distinction of JavaRosa-derived code from local
eyelidlessness May 27, 2024
aac8fe2
Break out `Scenario` client-traversal behavior
eyelidlessness May 27, 2024
dc04975
Break out engine form init from Scenario init
eyelidlessness May 27, 2024
29b9ded
Migrate existing JR/Scenario tests to Vitest assertions
eyelidlessness May 27, 2024
18cdf4d
More general engine traversal logic for existing Scenario functionality
eyelidlessness Apr 15, 2024
1a18f5a
Increased consistency with JavaRosa “event” types
eyelidlessness May 27, 2024
9999559
Temp: reject Root inititalization on uncaught error
eyelidlessness Apr 15, 2024
4fb1a25
All current JavaRosa `resources` fixtures
eyelidlessness Apr 15, 2024
b930955
Support loading JavaRosa `resources` fixtures in `Scenario.init`
eyelidlessness Apr 15, 2024
20724dd
Port all JavaRosa tests from ChoiceNameTest.java
eyelidlessness May 27, 2024
78c9aab
Dispose of Scenario-internal reactive computations on test cleanup
eyelidlessness Apr 16, 2024
6645558
Better error when attempting to answer unresolvable question
eyelidlessness Apr 16, 2024
6bd036e
First select test - generalize absolute/relative body refs, choice as…
eyelidlessness May 27, 2024
9a41ecd
Second select test, improve choice assertions to handle array/iterabl…
eyelidlessness Apr 16, 2024
5a61ae0
Third select test, add `Scenario.removeRepeat`
eyelidlessness Apr 16, 2024
6580f4d
Final select test in region “Select from repeat”
eyelidlessness Apr 16, 2024
5f8acdb
Better generalization of extending Vitest assertions
eyelidlessness May 27, 2024
abc3d31
Move parameterization of absolute ref substitution…
eyelidlessness Apr 24, 2024
0ded764
Port remainder of DynamicSelectUpdateTest.java
eyelidlessness Apr 24, 2024
2860f26
Scenario select tests - Move “ported as of” comment up
eyelidlessness Apr 25, 2024
d7ac19d
Porting FormDefSerializationTest.java - mostly skipped + context
eyelidlessness May 27, 2024
b824c66
Fix: correct Scenario fixture resources path
eyelidlessness Apr 25, 2024
4e963fc
Port first correctness test from PredicateCachingTest.java
eyelidlessness Apr 25, 2024
5e9fc30
Port next test from `PredicateCachingTest`, refine Scenario.createNew…
eyelidlessness Apr 25, 2024
ab62831
Port remainder of `PredicateCachingTest.java`
eyelidlessness May 27, 2024
e727cf1
Fix: don’t include non-relevant nodes as positional “event”s
eyelidlessness Apr 25, 2024
9d369b0
Port all of RepeatTest.java
eyelidlessness Apr 25, 2024
6793031
Fix: include missed `selecct1Dynamic` DSL overload
eyelidlessness Apr 26, 2024
617f6ed
Port tests from `PredicateCachingTest.java` with correctness assertions…
eyelidlessness Apr 26, 2024
258b4ea
First SelectChoiceTest.java port + `Scenario.getQuestionAtIndex`
eyelidlessness Apr 26, 2024
b8d2aa8
Port JavaRosa `SelectChoiceTest.java` tests exercising SelectChoice.g…
eyelidlessness May 27, 2024
c367551
Port remainder of SelectChoiceTest.java
eyelidlessness Apr 27, 2024
34f3151
Port SelectMultipleChoiceFilterTest.java
eyelidlessness May 27, 2024
2c09eee
Port SelectOneChoiceFilterTest.java
eyelidlessness Apr 27, 2024
893834e
First (top-down) `TriggerableDagTest.java` test port
eyelidlessness May 27, 2024
59e60c1
Second (top-down) `TriggerableDagTest.java` test port
eyelidlessness May 27, 2024
3ce1a79
`TriggerableDagTest.java` - next cycle detection test, first big surp…
eyelidlessness Apr 28, 2024
75aef32
`TriggerableDagTest.java` - restructure grouping of `parsing_forms_wi…
eyelidlessness Apr 28, 2024
6f88032
`TriggerableDagTest.java` - port and supplement constraint cycles test
eyelidlessness Apr 28, 2024
c6f5105
`TriggerableDagTest.java` - port “codependant” tests
eyelidlessness Apr 28, 2024
3880f3c
`TriggerableDagTest.java` - rest of cycles region, all concerned with…
eyelidlessness Apr 29, 2024
d7ae12e
Generalization of custom assertions matching some static aspect of an…
eyelidlessness Apr 29, 2024
0b0d753
Custom assertions: `toBeRelevant`, `toBeNonRelevant`
eyelidlessness May 27, 2024
02149b4
`TriggerableDagTest.java` - port first `relevant` test…
eyelidlessness May 27, 2024
0f0570e
`TriggerableDagTest.java` - port relevance exclusion test…
eyelidlessness May 27, 2024
99d6434
TriggerableDagTest.java: port relevance tests with surprising assertions
eyelidlessness Apr 29, 2024
9baef38
TriggerableDagTest.java - last test in relevance region
eyelidlessness Apr 29, 2024
ae5e693
Add followup note on non-relevant node value assertions
eyelidlessness Apr 29, 2024
006c3bf
Custom node state assertions for `readonly`…
eyelidlessness Apr 29, 2024
cedfafa
TriggerableDagTest.java - port first (only?) `readonly` test
eyelidlessness May 27, 2024
1a6f7df
TriggerableDagTest.java - port “Required and constraint” region
eyelidlessness May 27, 2024
38083b4
TriggerableDagTest.java - move older ported repeat tests into describ…
eyelidlessness Apr 29, 2024
b79ada6
TriggerableDagTest.java - port first (well, next) repeat test
eyelidlessness Apr 29, 2024
f9e74b2
TriggerableDagTest.java - port remainder of region “Adding or deletin…
eyelidlessness Apr 30, 2024
0eda13f
Regroup all tests from region “Adding or deleting repeats”
eyelidlessness Apr 30, 2024
0db4191
TriggerableDagTest.java - several more repeat tests ported…
eyelidlessness Apr 30, 2024
9905fc4
TriggerableDagTest.java - remainder of region: Deleting repeats
eyelidlessness May 1, 2024
b3bbe71
TriggerableDagTest.java - region: Adding repeats
eyelidlessness May 1, 2024
3300247
TriggerableDagTest.java - region: DAG limitations (cases that aren't …
eyelidlessness May 1, 2024
82c5716
Fix: mistaken placement of quote in ported JavaRosa form fixture DSL
eyelidlessness May 2, 2024
21e296e
Port remainder of TriggerableDagTest.java(!)
eyelidlessness May 2, 2024
16a6950
Port InstanceLoadEventsTest.java
eyelidlessness May 27, 2024
987ac3b
Port MultipleEventsTest.java
eyelidlessness May 2, 2024
fd7c7b7
Port OdkNewRepeatEventTest.java
eyelidlessness May 2, 2024
c9b82f1
RecordAudioActionTest.java - port second `odk:recordaudio` test
eyelidlessness May 2, 2024
80a23a8
Port remainder of RecordAudioActionTest.java
eyelidlessness May 2, 2024
54fd43b
Port SetGeopointActionTest.java
eyelidlessness May 2, 2024
c83a74c
Port SetValueActionTest.java
eyelidlessness May 3, 2024
7365f08
Port ReadOnlyCalculateTest.java
eyelidlessness May 27, 2024
ae8b1fc
FormDefTest.java - port first two (`constraint`) tests
eyelidlessness May 3, 2024
edd39f7
FormDefTest.java - port first repeat/relevant test
eyelidlessness May 27, 2024
50425df
FormDefTest.java - several more repeat/relevant tests
eyelidlessness May 27, 2024
f4e09d2
FormDefTest.java - port last test from repeat/relevant region
eyelidlessness May 4, 2024
c7e1f8a
FormDefTest.java - port first test of whether repeat instance can be …
eyelidlessness May 4, 2024
4f1021e
FormDefTest.java - port first test focused on interaction of repeat/o…
eyelidlessness May 27, 2024
c64cd01
FormDefTest.java - port second test focused on interaction of repeat/…
eyelidlessness May 4, 2024
6d07bdc
FormDefTest.java - port (as noop) last test: XPath function extensibi…
eyelidlessness May 4, 2024
4a4681a
Add support for assymetric assertions where `expected` is primitive
eyelidlessness May 5, 2024
27b41f7
Introduce `toStartWith` custom answer assertion
eyelidlessness May 5, 2024
ebd5c9b
Port QuestionPreloaderTest.java
eyelidlessness May 27, 2024
297ec0f
Port GeoAreaTest.java
eyelidlessness May 27, 2024
3a2d782
Port GeoDistanceTest.java
eyelidlessness May 6, 2024
d703546
Port (equivalent of) FormEntryModelTest.java
eyelidlessness May 6, 2024
e6351da
Port FormEntryPromptTest.java
eyelidlessness May 6, 2024
694b792
(Skipped) all entities tests
eyelidlessness May 6, 2024
9576030
Port MultiplePredicateTest.java
eyelidlessness May 6, 2024
29f0638
Port XFormSerializingVisitorTest.java
eyelidlessness May 27, 2024
4e805ac
Port IndexedRepeatRelativeRefsTest.java
eyelidlessness May 6, 2024
4884140
Partially port/skip SameRefDifferentInstancesIssue449Test.java
eyelidlessness May 6, 2024
203fa81
Port TriggersForRelativeRefsTest.java
eyelidlessness May 6, 2024
e710211
(Partially) port ChildVaccinationTest.java
eyelidlessness May 27, 2024
f0b383f
Port WhoVATest.java smoketests
eyelidlessness May 8, 2024
cba7a5c
Formally skip FormEntryControllerTest.java with notes
eyelidlessness May 8, 2024
7fd26b7
Add suite for form-wide functionality, tests for form title
eyelidlessness May 27, 2024
4f2dd82
Port ExternalSecondaryInstanceParseTest.java
eyelidlessness May 8, 2024
2cc1e46
Minimal port of XFormParserTest.java
eyelidlessness May 27, 2024
bdf7e65
Port Base64DecodeTest.java
eyelidlessness May 27, 2024
9de885c
Port CurrentFieldRefTest.java
eyelidlessness May 10, 2024
fd1678c
Port CurrentGroupCountRefTest.java
eyelidlessness May 10, 2024
5100d0d
Port CurrentTest.java
eyelidlessness May 10, 2024
d0aaa41
Port DigestTest.java
eyelidlessness May 27, 2024
9d23264
Port ExtractSignedTest.java
eyelidlessness May 27, 2024
9ca6cd1
Remove now-unnecessary delay on Scenario.init
eyelidlessness May 15, 2024
39d3b26
Remove outdated mention of “memos below”.
eyelidlessness May 15, 2024
7aea2c3
Update porting notes about all `InconsistentChildrenStateError` cases
eyelidlessness May 15, 2024
c85a897
Remove unused `fromReference` parameter to `getClosestRepeatRange`
eyelidlessness May 20, 2024
0b88fb5
Address SelectNode.currentState.value[].label regression
eyelidlessness May 20, 2024
ac10658
Fix: typo `factionalDays` -> `fractionalDays`
eyelidlessness May 20, 2024
ddb9a5b
Fix: correct grammar in JSDoc
eyelidlessness May 20, 2024
62c104a
Eliminate redundant destructuring alias
eyelidlessness May 20, 2024
9ad7dc7
Update `scenario` package README status section
eyelidlessness May 28, 2024
67bfe9e
Update JR comment copypasta in choice-name tests
eyelidlessness May 28, 2024
4f86060
Remove unused JR test function wrapper (`relativeBodyRefTest`)
eyelidlessness May 28, 2024
b0e7317
Update PORTING NOTES about XPathFuncExprRandomizeTest
eyelidlessness May 28, 2024
f29f57e
Porting note about JavaRosa’s `removeRepeat` error message
eyelidlessness May 28, 2024
2157c53
Fix typo: fitler -> filter
eyelidlessness May 28, 2024
554cf4c
Copy notes about `serializeAndDeserializeForm`
eyelidlessness May 28, 2024
a105968
Rephrase first doNotGetConfused
eyelidlessness May 28, 2024
6c5176c
Update SelectChoice.getChild porting notes per PR comment
eyelidlessness May 28, 2024
ae237aa
Inline setup of SelectMultipleChoiceFilterTest.java tests
eyelidlessness May 28, 2024
3d3e6a2
Rephrase removesIrrelevantAnswersAtAllLevels_withoutChangingOrder
eyelidlessness May 28, 2024
b4d1cfd
Inline setup in SelectOneChoiceFilterTest.java…
eyelidlessness May 28, 2024
8bd2302
Rephrase JR’s order_of_the_DAG_is_ensured
eyelidlessness May 28, 2024
29762af
Rephrase “read only” -> `readonly`
eyelidlessness May 28, 2024
1594704
Remove these comments
eyelidlessness May 29, 2024
ea4f509
Fix typo: “inf ields” -> "in fields”
eyelidlessness May 29, 2024
f9bedd3
`Scenario.getAnswerNode` -> `getInstanceNode`
eyelidlessness May 29, 2024
c997e85
Address relevant test which was intended to use a <repeat>
eyelidlessness May 29, 2024
a3f4a91
Update porting notes on contextualized count(abs path)…
eyelidlessness May 29, 2024
73c1f14
Rephrase a few test descriptions (“repeat group”, “triggerables”)
eyelidlessness May 29, 2024
bf0cc74
Update to range(1, …)
eyelidlessness May 29, 2024
5a23057
Confirmed intuition on this `nullValue()` check
eyelidlessness May 29, 2024
e4ed69f
Update porting notes to reflect edits as a “second load” condition
eyelidlessness May 29, 2024
e1fb3fb
Update porting notes about apparent intent of `jr:count` value casting
eyelidlessness May 29, 2024
c62bb83
Rephrase `repeatIsIrrelevant_whenRelevanceSetToFalse`
eyelidlessness May 29, 2024
c723dbe
“I think usually these kinds of comments get a JR prefix. That'd be h…
eyelidlessness May 29, 2024
a60e384
Add super helpful explanation of previously confusing error-on-read
eyelidlessness May 29, 2024
ca5afef
Include answer to open questions about error on empty select
eyelidlessness May 29, 2024
16e4f56
Rename Scenario .xhtml resource to .xml
eyelidlessness May 30, 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
11 changes: 11 additions & 0 deletions packages/common/src/lib/runtime-types/instance-predicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type AssertInstanceType = <T, U extends T>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Constructor: abstract new (...args: any[]) => U,
instance: T
) => asserts instance is U;

export const assertInstanceType: AssertInstanceType = (Constructor, instance) => {
if (!(instance instanceof Constructor)) {
throw new Error('Instance of unexpected type');
}
};
13 changes: 13 additions & 0 deletions packages/common/src/test/assertions/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { instanceArrayAssertion } from './instanceArrayAssertion.ts';
export { instanceAssertion } from './instanceAssertion.ts';
export { typeofAssertion } from './typeofAssertion.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 { extendExpect } from './vitest/extendExpect.ts';
export type {
CustomInspectable,
DeriveStaticVitestExpectExtension,
Inspectable,
} from './vitest/shared-extension-types.ts';
30 changes: 30 additions & 0 deletions packages/common/src/test/assertions/instanceArrayAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { AssertIs } from '../../../types/assertions/AssertIs.ts';
import type { ConstructorOf } from '../../../types/helpers';
import { instanceAssertion } from './instanceAssertion.ts';

/**
* Creates a type assertion function, used to validate both statically and at
* runtime that a value is an array, and each item in the array is an instance
* of the provided {@link Constructor}.
*/
export const instanceArrayAssertion = <T>(
Constructor: ConstructorOf<T>
): AssertIs<readonly T[]> => {
const assertInstance: AssertIs<T> = instanceAssertion(Constructor);

return (value) => {
if (!Array.isArray(value)) {
throw new Error(`Not an array of ${Constructor.name}: value itself is not an array`);
}

for (const [index, item] of value.entries()) {
try {
assertInstance(item);
} catch {
throw new Error(
`Not an array of ${Constructor.name}: item at index ${index} not an instance`
);
}
}
};
};
14 changes: 14 additions & 0 deletions packages/common/src/test/assertions/instanceAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { AssertIs } from '../../../types/assertions/AssertIs.ts';
import type { ConstructorOf } from '../../../types/helpers';

/**
* Creates a type assertion function, used to validate both statically and at
* runtime that a value is an instance of the provided {@link Constructor}.
*/
export const instanceAssertion = <T>(Constructor: ConstructorOf<T>): AssertIs<T> => {
return (value) => {
if (!(value instanceof Constructor)) {
throw new Error(`Not an instance of ${Constructor.name}`);
}
};
};
76 changes: 76 additions & 0 deletions packages/common/src/test/assertions/typeofAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { AnyConstructor, AnyFunction } from '../../../types/helpers.d.ts';

/**
* @see {@link Typeof}
*/
const TYPEOF = typeof ('' as unknown);

/**
* Used to derive the union of strings produced by a `typeof ...` expression.
* While this doesn't change frequently:
*
* - It has changed twice in recent memory (`"symbol"` and `"bigint"`)
* - It will likely change in the foreseeable future (`"record"` and `"tuple"`)
*
* Deriving this type helps to ensure {@link TypeofType} remains up to date.
*/
type Typeof = typeof TYPEOF;

/**
* Corresponds to values producing "function" in a `typeof ...` expression.
*
* Note that TypeScript will produce {@link Function} when the input type is
* `unknown`, but will produce a narrower type for some more specific inputs,
* such as a union between some non-function type and:
*
* - One or more specific function signatures, possibly also specifying its
* `this` context.
* - Any class, which may or may not be `abstract`.
*
* TypeScript will **also** narrow those cases with {@link typeofAssertion} with
* this more expanded type, but in many cases it would fail to do so if we only
* specify {@link Function}.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
type TypeofFunction = AnyConstructor | AnyFunction | Function;

/**
* Corresponds to values producing "object" in a `typeof ...` expression.
*/
type TypeofObject = object | null;

/**
* While {@link Typeof} can be derived, the type implied by each member of
* that union cannot. This is a best faith effort to represent the actual
* types corresponding to each case. By mapping the type, we can use the
* {@link typeofAssertion} factory to produce accurate derived types with
* minimal redundancy, and correct any discrepancies in one place as they
* might arise (noting that both {@link function} and {@link object} are
* mapped to more complex types than one might assume from their names).
*/
interface TypeofTypes {
bigint: bigint;
boolean: boolean;
function: TypeofFunction;
number: number;
object: TypeofObject;
string: string;
symbol: symbol;
undefined: undefined;
}

type TypeofType<T extends Typeof> = TypeofTypes[T];

type TypeofAssertion<T extends Typeof> = <U>(
value: U
) => asserts value is Extract<TypeofType<T>, U>;

export const typeofAssertion = <T extends Typeof>(expected: T): TypeofAssertion<T> => {
return (value) => {
const actual = typeof value;

if (actual !== expected) {
throw new Error(`Expected typeof value to be ${expected}, got ${actual}`);
}
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { SyncExpectationResult } from 'vitest';
import type { AssertIs } from '../../../../types/assertions/AssertIs.ts';
import { expandSimpleExpectExtensionResult } from './expandSimpleExpectExtensionResult.ts';
import type { ExpectExtensionMethod } 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 AsymmetricTypedExpectExtension<Actual = unknown, Expected = Actual> {
readonly extensionMethod: ExpectExtensionMethod<unknown, unknown, SyncExpectationResult>;

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

this.extensionMethod = expandSimpleExpectExtensionResult(validatedMethod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { inspect } from './inspect.ts';
import type { Inspectable } from './shared-extension-types.ts';

interface InspectableComparisonErrorOptions {
readonly comparisonQualifier?: string;
readonly details?: string;
}

/**
* Provides a general mechanism for reporting assertion failures in a consistent
* format, for the general class of Vitest assertion extensions which fall into
* a broad category of comparisons, where failure reports will tend to follow a
* format of:
*
* > Expected $actual to $comparisonVerb $expected
* > $comparisonQualifier? ...$details?
*/
export class InspectableComparisonError extends Error {
constructor(
actual: Inspectable,
expected: Inspectable,
comparisonVerb: string,
options: InspectableComparisonErrorOptions = {}
) {
const { comparisonQualifier, details } = options;

const messageParts = [
'Expected',
inspect(actual),
'to',
comparisonVerb,
inspect(expected),
comparisonQualifier,
details,
].filter((value): value is string => typeof value === 'string');

super(messageParts.join(' '));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { JSONValue } from '../../../../types/JSONValue.ts';
import { inspect } from './inspect.ts';
import type { Inspectable } from './shared-extension-types.ts';

interface InspectableStaticConditionErrorOptions {
readonly details?: string;
}

export class InspectableStaticConditionError extends Error {
constructor(
actual: Inspectable,
expectedCondition: JSONValue,
options: InspectableStaticConditionErrorOptions = {}
) {
const { details } = options;

const messageParts = [
'Expected',
inspect(actual),
'to equal',
JSON.stringify(expectedCondition),
details,
].filter((value): value is string => typeof value === 'string');

super(messageParts.join(' '));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { JestAssertion, SyncExpectationResult } from 'vitest';
import { expect } from 'vitest';
import type { JSONObject } from '../../../../types/JSONValue.ts';
import type { AssertIs } from '../../../../types/assertions/AssertIs.ts';
import { assertVoidExpectedArgument } from './assertVoidExpectedArgument.ts';
import { expandSimpleExpectExtensionResult } from './expandSimpleExpectExtensionResult.ts';
import type { ExpectExtensionMethod } from './shared-extension-types.ts';
import { validatedExtensionMethod } from './validatedExtensionMethod.ts';

/**
* Produces a callable `expect` extension implementation, which matches a
* provided `staticCondition` object. This is effectively a wrapper around
* {@link JestAssertion.toMatchObject | `expect(...).toMatchObject(staticCondition)`},
* generated by the custom assertion's definition, and producing the return type
* expected by all of our custom assertion interfaces.
*/
const staticConditionExtensionMethodFactory = <Parameter>(
staticCondition: JSONObject
): ExpectExtensionMethod<Parameter, void> => {
return (actual) => {
try {
expect(actual).toMatchObject(staticCondition);

return true;
} catch (error) {
if (error instanceof Error) {
return error;
}

return new Error('Unknown error in assertion');
}
};
};

/**
* Generalizes definition of a Vitest `expect` API extension where the assertion
* expects a specific type for its `actual` parameter, and:
*
* - Implements an assertion checking some statically known condition of the
* `actual` argument, as represented by an object suitable for use in
* {@link JestAssertion.toMatchObject | `expect(...).toMatchObject(expectedStaticCondition)`}
*
* - Automatically perfoms runtime validation of that parameter, 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
*
* @todo Reconsider naming and language around "static"-ness. The idea here is
* that the `expected` parameter is defined upfront by the extension, not passed
* as a parameter at the assertion's call site.
*/
export class StaticConditionExpectExtension<
StaticCondition extends JSONObject,
Parameter extends StaticCondition = StaticCondition,
> {
readonly extensionMethod: ExpectExtensionMethod<unknown, unknown, SyncExpectationResult>;

constructor(
readonly validateArgument: AssertIs<Parameter>,
readonly expectedStaticCondition: StaticCondition
) {
const validatedMethod = validatedExtensionMethod(
validateArgument,
assertVoidExpectedArgument,
staticConditionExtensionMethodFactory(expectedStaticCondition)
);

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

/**
* Generalizes definition of a Vitest `expect` API extension where the assertion
* expects the same type for both 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 SymmetricTypedExpectExtension<Parameter = unknown> {
readonly extensionMethod: ExpectExtensionMethod<unknown, unknown, SyncExpectationResult>;

constructor(
readonly validateArgument: AssertIs<Parameter>,
extensionMethod: ExpectExtensionMethod<Parameter, Parameter>
) {
const validatedMethod = validatedExtensionMethod(
validateArgument,
validateArgument,
extensionMethod
);

this.extensionMethod = expandSimpleExpectExtensionResult(validatedMethod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type AssertVoidExpectedArgument = (
args: readonly unknown[]
) => asserts args is readonly [expected?: undefined];

export const assertVoidExpectedArgument: AssertVoidExpectedArgument = (args) => {
// This accounts for awkwardness around the generic assertion types, where the
// expected argument (as optional first item in a `...rest` array) may be
// present but undefined, to allow for a common call site shape.
if (args.length === 1 && args[0] === undefined) {
return;
}

if (args.length > 0) {
throw new Error('Assertion does not accept any `expected` arguments');
}
};
Loading