From 3d4548450a1393becc7ea244d16cea4d99b05393 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 22 Apr 2022 14:29:23 -0700 Subject: [PATCH] refactor(compiler-cli): support schemas in standalone components (#45752) This commit adds support for `@Component.schemas` in both JIT and AOT. PR Close #45752 --- goldens/public-api/core/index.md | 2 + .../src/ngtsc/annotations/common/index.ts | 1 + .../ngtsc/annotations/common/src/schema.ts | 51 +++++++++++++++++++ .../annotations/component/src/handler.ts | 20 +++++++- .../annotations/component/src/metadata.ts | 4 +- .../annotations/directive/src/handler.ts | 1 + .../annotations/ng_module/src/handler.ts | 39 ++------------ .../src/ngtsc/metadata/src/api.ts | 5 ++ .../src/ngtsc/metadata/src/dts.ts | 2 + .../src/ngtsc/scope/src/standalone.ts | 2 +- .../src/ngtsc/scope/test/local_spec.ts | 1 + .../src/ngtsc/typecheck/testing/index.ts | 1 + .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 3 -- .../test/ngtsc/standalone_spec.ts | 47 +++++++++++++++++ packages/core/src/metadata/directives.ts | 3 ++ packages/core/src/render3/jit/directive.ts | 11 ++++ .../core/test/acceptance/ng_module_spec.ts | 33 ++++++++++++ .../core/test/acceptance/standalone_spec.ts | 30 ++++++++++- 18 files changed, 213 insertions(+), 43 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index 1b3f375d633..5e957d96ebc 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -187,6 +187,8 @@ export interface Component extends Directive { moduleId?: string; preserveWhitespaces?: boolean; // (undocumented) + schemas?: SchemaMetadata[]; + // (undocumented) standalone?: boolean; styles?: string[]; styleUrls?: string[]; diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/index.ts b/packages/compiler-cli/src/ngtsc/annotations/common/index.ts index b4049f93037..f6f6ba4deea 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/index.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/index.ts @@ -13,4 +13,5 @@ export * from './src/evaluation'; export * from './src/factory'; export * from './src/metadata'; export * from './src/references_registry'; +export * from './src/schema'; export * from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts new file mode 100644 index 00000000000..6aadfc45736 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts @@ -0,0 +1,51 @@ +/** + * @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 {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '@angular/compiler'; +import ts from 'typescript'; + +import {Reference} from '../../../imports'; +import {PartialEvaluator} from '../../../partial_evaluator'; + +import {createValueHasWrongTypeError} from './diagnostics'; + +export function extractSchemas( + rawExpr: ts.Expression, evaluator: PartialEvaluator, context: string): SchemaMetadata[] { + const schemas: SchemaMetadata[] = []; + const result = evaluator.evaluate(rawExpr); + if (!Array.isArray(result)) { + throw createValueHasWrongTypeError(rawExpr, result, `${context}.schemas must be an array`); + } + + for (const schemaRef of result) { + if (!(schemaRef instanceof Reference)) { + throw createValueHasWrongTypeError( + rawExpr, result, `${context}.schemas must be an array of schemas`); + } + const id = schemaRef.getIdentityIn(schemaRef.node.getSourceFile()); + if (id === null || schemaRef.ownedByModuleGuess !== '@angular/core') { + throw createValueHasWrongTypeError( + rawExpr, result, `${context}.schemas must be an array of schemas`); + } + // Since `id` is the `ts.Identifer` within the schema ref's declaration file, it's safe to + // use `id.text` here to figure out which schema is in use. Even if the actual reference was + // renamed when the user imported it, these names will match. + switch (id.text) { + case 'CUSTOM_ELEMENTS_SCHEMA': + schemas.push(CUSTOM_ELEMENTS_SCHEMA); + break; + case 'NO_ERRORS_SCHEMA': + schemas.push(NO_ERRORS_SCHEMA); + break; + default: + throw createValueHasWrongTypeError( + rawExpr, schemaRef, `'${schemaRef.debugName}' is not a valid ${context} schema`); + } + } + return schemas; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 365632c1fe6..17ca0077251 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, compileClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler'; +import {AnimationTriggerNames, compileClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler'; import ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; @@ -26,7 +26,7 @@ import {TypeCheckContext} from '../../../typecheck/api'; import {ExtendedTemplateChecker} from '../../../typecheck/extended/api'; import {getSourceFile} from '../../../util/src/typescript'; import {Xi18nContext} from '../../../xi18n'; -import {compileDeclareFactory, compileNgFactoryDefField, compileResults, extractClassMetadata, findAngularDecorator, getDirectiveDiagnostics, getProviderDiagnostics, isExpressionForwardReference, readBaseClass, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, wrapFunctionExpressionsInParens} from '../../common'; +import {compileDeclareFactory, compileNgFactoryDefField, compileResults, extractClassMetadata, extractSchemas, findAngularDecorator, getDirectiveDiagnostics, getProviderDiagnostics, isExpressionForwardReference, readBaseClass, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, wrapFunctionExpressionsInParens} from '../../common'; import {extractDirectiveMetadata, parseFieldArrayValue} from '../../directive'; import {NgModuleSymbol} from '../../ng_module'; @@ -288,6 +288,20 @@ export class ComponentDecoratorHandler implements } } + let schemas: SchemaMetadata[]|null = null; + if (component.has('schemas') && !metadata.isStandalone) { + if (diagnostics === undefined) { + diagnostics = []; + } + diagnostics.push(makeDiagnostic( + ErrorCode.COMPONENT_NOT_STANDALONE, component.get('schemas')!, + `'schemas' is only valid on a component that is standalone.`)); + } else if (component.has('schemas')) { + schemas = extractSchemas(component.get('schemas')!, this.evaluator, 'Component'); + } else if (metadata.isStandalone) { + schemas = []; + } + // Parse the template. // If a preanalyze phase was executed, the template may already exist in parsed form, so check // the preanalyzeTemplateCache. @@ -430,6 +444,7 @@ export class ComponentDecoratorHandler implements animationTriggerNames, rawImports, resolvedImports, + schemas, }, diagnostics, }; @@ -468,6 +483,7 @@ export class ComponentDecoratorHandler implements isStandalone: analysis.meta.isStandalone, imports: analysis.resolvedImports, animationTriggerNames: analysis.animationTriggerNames, + schemas: analysis.schemas, }); this.resourceRegistry.registerResources(analysis.resources, node); diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts index 80253931400..ef693411f85 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, R3ClassMetadata, R3ComponentMetadata, R3TemplateDependency, R3TemplateDependencyMetadata} from '@angular/compiler'; +import {AnimationTriggerNames, R3ClassMetadata, R3ComponentMetadata, R3TemplateDependency, R3TemplateDependencyMetadata, SchemaMetadata} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../../imports'; @@ -68,6 +68,8 @@ export interface ComponentAnalysisData { rawImports: ts.Expression|null; resolvedImports: Reference[]|null; + + schemas: SchemaMetadata[]|null; } export type ComponentResolutionData = diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index ae2d86a2b7c..048084a1408 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -148,6 +148,7 @@ export class DirectiveDecoratorHandler implements animationTriggerNames: null, isStandalone: analysis.meta.isStandalone, imports: null, + schemas: null, }); this.injectableRegistry.registerInjectable(node); diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts index 09c06b43269..0a9513b4893 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts @@ -20,7 +20,7 @@ import {LocalModuleScopeRegistry, ScopeData} from '../../../scope'; import {FactoryTracker} from '../../../shims/api'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform'; import {getSourceFile} from '../../../util/src/typescript'; -import {combineResolvers, compileDeclareFactory, compileNgFactoryDefField, createValueHasWrongTypeError, extractClassMetadata, findAngularDecorator, forwardRefResolver, getProviderDiagnostics, getValidConstructorDependencies, isExpressionForwardReference, ReferencesRegistry, resolveProvidersRequiringFactory, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens, wrapTypeReference} from '../../common'; +import {combineResolvers, compileDeclareFactory, compileNgFactoryDefField, createValueHasWrongTypeError, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getProviderDiagnostics, getValidConstructorDependencies, isExpressionForwardReference, ReferencesRegistry, resolveProvidersRequiringFactory, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens, wrapTypeReference} from '../../common'; export interface NgModuleAnalysis { mod: R3NgModuleMetadata; @@ -237,40 +237,9 @@ export class NgModuleDecoratorHandler implements bootstrapRefs = this.resolveTypeList(expr, bootstrapMeta, name, 'bootstrap', 0).references; } - const schemas: SchemaMetadata[] = []; - if (ngModule.has('schemas')) { - const rawExpr = ngModule.get('schemas')!; - const result = this.evaluator.evaluate(rawExpr); - if (!Array.isArray(result)) { - throw createValueHasWrongTypeError(rawExpr, result, `NgModule.schemas must be an array`); - } - - for (const schemaRef of result) { - if (!(schemaRef instanceof Reference)) { - throw createValueHasWrongTypeError( - rawExpr, result, 'NgModule.schemas must be an array of schemas'); - } - const id = schemaRef.getIdentityIn(schemaRef.node.getSourceFile()); - if (id === null || schemaRef.ownedByModuleGuess !== '@angular/core') { - throw createValueHasWrongTypeError( - rawExpr, result, 'NgModule.schemas must be an array of schemas'); - } - // Since `id` is the `ts.Identifer` within the schema ref's declaration file, it's safe to - // use `id.text` here to figure out which schema is in use. Even if the actual reference was - // renamed when the user imported it, these names will match. - switch (id.text) { - case 'CUSTOM_ELEMENTS_SCHEMA': - schemas.push(CUSTOM_ELEMENTS_SCHEMA); - break; - case 'NO_ERRORS_SCHEMA': - schemas.push(NO_ERRORS_SCHEMA); - break; - default: - throw createValueHasWrongTypeError( - rawExpr, schemaRef, `'${schemaRef.debugName}' is not a valid NgModule schema`); - } - } - } + const schemas = ngModule.has('schemas') ? + extractSchemas(ngModule.get('schemas')!, this.evaluator, 'NgModule') : + []; let id: Expression|null = null; if (ngModule.has('id')) { diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index 64151c7196b..f481acea5b7 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -156,6 +156,11 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { * For standalone components, the list of imported types. */ imports: Reference[]|null; + + /** + * For standalone components, the list of schemas declared. + */ + schemas: SchemaMetadata[]|null; } /** diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index 1d4a0d5b9bb..a769605c1b2 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -117,6 +117,8 @@ export class DtsMetadataReader implements MetadataReader { // Imports are tracked in metadata only for template type-checking purposes, // so standalone components from .d.ts files don't have any. imports: null, + // The same goes for schemas. + schemas: null, }; } diff --git a/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts b/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts index a2c8915dba1..1489778316b 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts @@ -100,7 +100,7 @@ export class StandaloneComponentScopeReader implements ComponentScopeReader { component: clazz, dependencies: Array.from(dependencies), isPoisoned, - schemas: [], + schemas: clazzMeta.schemas ?? [], }); } diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts index e8b7186d4fc..b29660fc3c0 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts @@ -296,6 +296,7 @@ function fakeDirective(ref: Reference): DirectiveMeta { animationTriggerNames: null, isStandalone: false, imports: null, + schemas: null, }; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index bad22cdbc27..8599e231d6d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -649,6 +649,7 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio animationTriggerNames: null, isStandalone: false, imports: null, + schemas: null, }); } else if (decl.type === 'pipe') { scope.dependencies.push({ diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index fa03435b8a5..f26f31ac72c 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -4419,14 +4419,11 @@ function allTests(os: string) { it('should not include `schemas` in component and module defs', () => { env.write('test.ts', ` import {Component, NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; - @Component({ selector: 'comp', template: '', - schemas: [NO_ERRORS_SCHEMA], }) class MyComp {} - @NgModule({ declarations: [MyComp], schemas: [NO_ERRORS_SCHEMA], diff --git a/packages/compiler-cli/test/ngtsc/standalone_spec.ts b/packages/compiler-cli/test/ngtsc/standalone_spec.ts index f0e55634ae6..d8c03cd618a 100644 --- a/packages/compiler-cli/test/ngtsc/standalone_spec.ts +++ b/packages/compiler-cli/test/ngtsc/standalone_spec.ts @@ -116,6 +116,53 @@ runInEachFileSystem(() => { expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[TestDir]'); }); + it('should compile a standalone component with schema support', () => { + env.write('test.ts', ` + import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + standalone: true, + template: '', + schemas: [NO_ERRORS_SCHEMA], + }) + export class TestCmp {} + `); + env.driveMain(); + + // verify that there are no compilation errors + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + + // verify generated code for the unknown element + const jsCode = env.getContents('test.js'); + expect(jsCode).toContain('i0.ɵɵelement(0, "is-unknown");'); + + // verify schemas are not included in the generated code + const jsCodeAoT = jsCode.slice( + 0, jsCode.indexOf('(function () { (typeof ngDevMode === "undefined" || ngDevMode)')); + expect(jsCodeAoT).not.toContain('schemas: [NO_ERRORS_SCHEMA]'); + }); + + it('should error when a non-standalone component tries to use schemas', () => { + env.write('test.ts', ` + import {Component, NO_ERRORS_SCHEMA} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: '
', + schemas: [NO_ERRORS_SCHEMA], + }) + export class TestCmp {} + `); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE)); + expect(diags[0].messageText) + .toBe(`'schemas' is only valid on a component that is standalone.`); + expect(getSourceCodeForDiagnostic(diags[0])).toEqual('[NO_ERRORS_SCHEMA]'); + }); + it('should compile a standalone component that imports an NgModule', () => { env.write('test.ts', ` import {Component, Directive, NgModule} from '@angular/core'; diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index ad989a2fc48..c9524820112 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -13,6 +13,7 @@ import {compileComponent, compileDirective} from '../render3/jit/directive'; import {compilePipe} from '../render3/jit/pipe'; import {makeDecorator, makePropDecorator, TypeDecorator} from '../util/decorators'; +import {SchemaMetadata} from './schema'; import {ViewEncapsulation} from './view'; @@ -560,6 +561,8 @@ export interface Component extends Directive { // TODO: better type here! imports?: (Type|any[])[]; + + schemas?: SchemaMetadata[]; } /** diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index a5688b2dc24..f4740cfb851 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -167,6 +167,17 @@ export function compileComponent(type: Type, metadata: Component): void { const scopes = transitiveScopesFor(type.ngSelectorScope); patchComponentDefWithScope(ngComponentDef, scopes); } + + if (metadata.schemas) { + if (metadata.standalone) { + ngComponentDef.schemas = metadata.schemas; + } else { + throw new Error(`The 'schemas' was specified for the ${ + stringifyForError(type)} but is only valid on a component that is standalone.`); + } + } else if (metadata.standalone) { + ngComponentDef.schemas = []; + } } return ngComponentDef; }, diff --git a/packages/core/test/acceptance/ng_module_spec.ts b/packages/core/test/acceptance/ng_module_spec.ts index 5d26ce24135..db111d7c8a9 100644 --- a/packages/core/test/acceptance/ng_module_spec.ts +++ b/packages/core/test/acceptance/ng_module_spec.ts @@ -212,6 +212,39 @@ describe('NgModule', () => { expect(spy.calls.mostRecent().args[0]).toMatch(/'custom-el' is not a known element/); }); + it('should log an error about unknown element for a standalone component without CUSTOM_ELEMENTS_SCHEMA', + () => { + @Component({ + template: ``, + standalone: true, + }) + class MyComp { + } + + const spy = spyOn(console, 'error'); + TestBed.configureTestingModule({imports: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + expect(spy.calls.mostRecent().args[0]).toMatch(/'custom-el' is not a known element/); + }); + + it('should not log an error about unknown element for a standalone component with CUSTOM_ELEMENTS_SCHEMA', + () => { + @Component({ + template: ``, + standalone: true, + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + class MyComp { + } + + const spy = spyOn(console, 'error'); + TestBed.configureTestingModule({imports: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + expect(spy).not.toHaveBeenCalled(); + }); + it('should log an error about unknown element without CUSTOM_ELEMENTS_SCHEMA for element without dash in tag name', () => { @Component({template: ``}) diff --git a/packages/core/test/acceptance/standalone_spec.ts b/packages/core/test/acceptance/standalone_spec.ts index 68331d20d86..452ea614e48 100644 --- a/packages/core/test/acceptance/standalone_spec.ts +++ b/packages/core/test/acceptance/standalone_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Component, createEnvironmentInjector, Directive, EnvironmentInjector, forwardRef, Injector, Input, NgModule, OnInit, Pipe, PipeTransform, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, createEnvironmentInjector, Directive, EnvironmentInjector, forwardRef, Injector, Input, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; describe('standalone components, directives and pipes', () => { @@ -446,4 +446,32 @@ describe('standalone components, directives and pipes', () => { fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe('(standalone component)'); }); + + describe('schemas', () => { + it('should allow schemas in standalone component', () => { + @Component({ + standalone: true, + template: '', + schemas: [NO_ERRORS_SCHEMA] + }) + class AppCmp { + } + + const fixture = TestBed.createComponent(AppCmp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toBe(''); + }); + + it('should error when schemas are specified for a non-standalone component', () => { + @Component({template: '', schemas: [NO_ERRORS_SCHEMA]}) + class AppCmp { + } + + expect(() => { + TestBed.createComponent(AppCmp); + }) + .toThrowError( + `The 'schemas' was specified for the AppCmp but is only valid on a component that is standalone.`); + }); + }); });