-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #110 from getodk/jr-scenario-ports/kitchen-sink
Port JavaRosa `Scenario` test suites
- Loading branch information
Showing
232 changed files
with
135,278 additions
and
879 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
packages/common/src/lib/runtime-types/instance-predicates.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
30
packages/common/src/test/assertions/instanceArrayAssertion.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
); | ||
} | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
}; | ||
}; |
37 changes: 37 additions & 0 deletions
37
packages/common/src/test/assertions/vitest/AsymmetricTypedExpectExtension.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
packages/common/src/test/assertions/vitest/InspectableComparisonError.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(' ')); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/common/src/test/assertions/vitest/InspectableStaticConditionError.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(' ')); | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
packages/common/src/test/assertions/vitest/StaticConditionExpectExtension.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
packages/common/src/test/assertions/vitest/SymmetricTypedExpectExtension.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
packages/common/src/test/assertions/vitest/assertVoidExpectedArgument.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
}; |
Oops, something went wrong.