forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(core): add type signature test for signal input API (angular#53571)
Adds tests that allow us to ensure that the `input` API works as expected and that resulting return types match our expectations- without silently regressing in the future, or missing potential edge-cases. Testing signatures is hard because of covariance and contravariance, especially when it comes to the different semantics of `ReadT` and `WriteT` of input signals. We enable reliable testing by validating the `d.ts` of the "fake directive class". This ensures clear results, compared to relying on e.g. type assertions that might accidentally/silently pass due to covariance/contravariance or biavariance in the type system. PR Close angular#53571
- Loading branch information
1 parent
61447da
commit 8cc88ad
Showing
3 changed files
with
184 additions
and
0 deletions.
There are no files selected for viewing
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,25 @@ | ||
load("//tools:defaults.bzl", "nodejs_test", "ts_library") | ||
|
||
ts_library( | ||
name = "signal_input_signature_test_lib", | ||
srcs = ["signal_input_signature_test.ts"], | ||
deps = ["//packages/core"], | ||
) | ||
|
||
ts_library( | ||
name = "type_tester_lib", | ||
srcs = ["type_tester.ts"], | ||
deps = [ | ||
"@npm//@types/node", | ||
"@npm//typescript", | ||
], | ||
) | ||
|
||
nodejs_test( | ||
name = "type_test", | ||
data = [ | ||
":signal_input_signature_test_lib", | ||
":type_tester_lib", | ||
], | ||
entry_point = ":type_tester.ts", | ||
) |
98 changes: 98 additions & 0 deletions
98
packages/core/test/authoring/signal_input_signature_test.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,98 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
/** | ||
* @fileoverview | ||
* This file contains various signal `input()` patterns and ensures | ||
* the resulting types match our expectations (via comments asserting the `.d.ts`). | ||
*/ | ||
|
||
import {input} from '../../src/authoring/input'; | ||
// import preserved to simplify `.d.ts` emit and simplify the `type_tester` logic. | ||
import {InputSignal} from '../../src/authoring/input_signal'; | ||
|
||
export class InputSignatureTest { | ||
/** string | undefined */ | ||
noInitialValueExplicitRead = input<string>(); | ||
/** boolean */ | ||
initialValueBooleanNoType = input(false); | ||
/** string */ | ||
initialValueStringNoType = input('bla'); | ||
/** number */ | ||
initialValueNumberNoType = input(0); | ||
/** string[] */ | ||
initialValueObjectNoType = input([] as string[]); | ||
/** number */ | ||
initialValueEmptyOptions = input(1, {}); | ||
|
||
/** @internal */ | ||
// @ts-expect-error Transform is needed | ||
__explicitWriteTWithoutTransformForbidden = input<string, string>('bla__'); | ||
|
||
/** number, string | number */ | ||
noInitialValueWithTransform = input.required({transform: (_v: number|string) => 0}); | ||
|
||
/** number, string | number */ | ||
initialValueWithTransform = input(0, {transform: (_v: number|string) => 0}); | ||
|
||
/** boolean | undefined, string | boolean */ | ||
undefinedInitialValueWithTransform = input(undefined, {transform: (_v: boolean|string) => true}); | ||
|
||
/** {works: boolean;}, string | boolean */ | ||
complexTransformWithInitialValue = input({works: true}, { | ||
transform: (_v: boolean|string) => ({works: !!_v}), | ||
}); | ||
|
||
/** RegExp */ | ||
nonPrimitiveInitialValue = input(/default regex/); | ||
|
||
/** string, string | null */ | ||
requiredExplicitReadAndWriteButNoTransform = | ||
input.required<string, string|null>({transform: _v => ''}); | ||
|
||
/** string, string | null */ | ||
withInitialValueExplicitReadAndWrite = input<string, string|null>('', {transform: bla => ''}); | ||
|
||
/** string | undefined */ | ||
withNoInitialValue = input<string>(); | ||
|
||
/** string */ | ||
requiredNoInitialValue = input.required<string>(); | ||
/** string | undefined */ | ||
requiredNoInitialValueExplicitUndefined = input.required<string|undefined>(); | ||
|
||
/** string, string | boolean */ | ||
requiredWithTransform = | ||
input.required<string, string|boolean>({transform: (v: string|boolean) => ''}); | ||
|
||
/** @internal */ | ||
__requiredWithTransformButNoWriteT = input.required<string>({ | ||
// @ts-expect-error | ||
transform: (v: string|boolean) => '' | ||
}); | ||
|
||
/** string, string | boolean */ | ||
requiredWithTransformInferenceNoExplicitGeneric = | ||
input.required({transform: (v: string|boolean) => ''}); | ||
|
||
// Unknown as `WriteT` is acceptable because the user explicitly opted into handling | ||
// the transform- so they will need to work with the `unknown` values. | ||
/** string, unknown */ | ||
requiredTransformButNoTypes = input.required({transform: (v) => ''}); | ||
|
||
/** unknown */ | ||
noInitialValueNoType = input(); | ||
/** string */ | ||
requiredNoInitialValueNoType = input.required<string>(); | ||
|
||
/** @internal */ | ||
__shouldErrorIfInitialValueWithRequired = input.required({ | ||
// @ts-expect-error | ||
initialValue: 0, | ||
}); | ||
} |
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,61 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import fs from 'fs'; | ||
import path from 'path'; | ||
import ts from 'typescript'; | ||
import url from 'url'; | ||
|
||
const containingDir = path.dirname(url.fileURLToPath(import.meta.url)); | ||
const testDtsFile = path.join(containingDir, 'signal_input_signature_test.d.ts'); | ||
|
||
async function main() { | ||
const fileContent = fs.readFileSync(testDtsFile, 'utf8'); | ||
const sourceFile = ts.createSourceFile('test.ts', fileContent, ts.ScriptTarget.ESNext, true); | ||
const testClazz = | ||
sourceFile.statements.find((s): s is ts.ClassDeclaration => ts.isClassDeclaration(s))!; | ||
let failing = false; | ||
|
||
for (const member of testClazz.members) { | ||
if (!ts.isPropertyDeclaration(member)) { | ||
continue; | ||
} | ||
|
||
const leadingCommentRanges = ts.getLeadingCommentRanges(sourceFile.text, member.getFullStart()); | ||
const leadingComments = leadingCommentRanges?.map(r => sourceFile.text.substring(r.pos, r.end)); | ||
|
||
if (leadingComments === undefined || leadingComments.length === 0) { | ||
throw new Error(`No expected type for: ${member.name.getText()}`); | ||
} | ||
|
||
// strip comment start, and beginning (plus whitespace). | ||
let expectedTypeComment = leadingComments[0].replace(/(^\/\*\*?\s*|\s*\*+\/$)/g, ''); | ||
// expand shorthands where ReadT is the same as WriteT. | ||
if (!expectedTypeComment.includes(',')) { | ||
expectedTypeComment = `${expectedTypeComment}, ${expectedTypeComment}`; | ||
} | ||
|
||
const expectedType = `InputSignal<${expectedTypeComment}>`; | ||
// strip excess whitespace or newlines. | ||
const got = member.type?.getText().replace(/(\n+|\s\s+)/g, ''); | ||
|
||
if (expectedType !== got) { | ||
console.error(`${member.name.getText()}: expected: ${expectedType}, got: ${got}`); | ||
failing = true; | ||
} | ||
} | ||
|
||
if (failing) { | ||
throw new Error('Failing assertions'); | ||
} | ||
} | ||
|
||
main().catch(e => { | ||
console.error(e); | ||
process.exitCode = 1; | ||
}); |