feat(ivy): compile queries in ngtsc (#24862)
This commit adds support for @ContentChild[ren] and @ViewChild[ren] in ngtsc. Previously queries were ignored. PR Close #24862
This commit is contained in:
parent
6eb6ac7c12
commit
76f8f78920
|
@ -11,11 +11,11 @@ import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Decorator, ReflectionHost} from '../../host';
|
import {Decorator, ReflectionHost} from '../../host';
|
||||||
import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
|
import {filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
|
||||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||||
|
|
||||||
import {ResourceLoader} from './api';
|
import {ResourceLoader} from './api';
|
||||||
import {extractDirectiveMetadata} from './directive';
|
import {extractDirectiveMetadata, extractQueriesFromDecorator, queriesFromFields} from './directive';
|
||||||
import {SelectorScopeRegistry} from './selector_scope';
|
import {SelectorScopeRegistry} from './selector_scope';
|
||||||
import {isAngularCore, unwrapExpression} from './util';
|
import {isAngularCore, unwrapExpression} from './util';
|
||||||
|
|
||||||
|
@ -59,9 +59,9 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||||
|
|
||||||
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
|
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
|
||||||
// on it.
|
// on it.
|
||||||
const directiveMetadata =
|
const directiveResult =
|
||||||
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
|
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
|
||||||
if (directiveMetadata === undefined) {
|
if (directiveResult === undefined) {
|
||||||
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
|
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
|
||||||
// case, compilation of the decorator is skipped. Returning an empty object signifies
|
// case, compilation of the decorator is skipped. Returning an empty object signifies
|
||||||
// that no analysis was produced.
|
// that no analysis was produced.
|
||||||
|
@ -69,7 +69,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, read the `@Component`-specific fields.
|
// Next, read the `@Component`-specific fields.
|
||||||
const component = reflectObjectLiteral(meta);
|
const {decoratedElements, decorator: component, metadata} = directiveResult;
|
||||||
|
|
||||||
let templateStr: string|null = null;
|
let templateStr: string|null = null;
|
||||||
if (component.has('templateUrl')) {
|
if (component.has('templateUrl')) {
|
||||||
|
@ -109,15 +109,29 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
|
||||||
|
|
||||||
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
|
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
|
||||||
// when this component appears in an `@NgModule` scope, its selector can be determined.
|
// when this component appears in an `@NgModule` scope, its selector can be determined.
|
||||||
if (directiveMetadata.selector !== null) {
|
if (metadata.selector !== null) {
|
||||||
this.scopeRegistry.registerSelector(node, directiveMetadata.selector);
|
this.scopeRegistry.registerSelector(node, metadata.selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the list of view queries.
|
||||||
|
const coreModule = this.isCore ? undefined : '@angular/core';
|
||||||
|
const viewChildFromFields = queriesFromFields(
|
||||||
|
filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.checker);
|
||||||
|
const viewChildrenFromFields = queriesFromFields(
|
||||||
|
filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.checker);
|
||||||
|
const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields];
|
||||||
|
|
||||||
|
if (component.has('queries')) {
|
||||||
|
const queriesFromDecorator = extractQueriesFromDecorator(
|
||||||
|
component.get('queries') !, this.reflector, this.checker, this.isCore);
|
||||||
|
viewQueries.push(...queriesFromDecorator.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
analysis: {
|
analysis: {
|
||||||
...directiveMetadata,
|
...metadata,
|
||||||
template,
|
template,
|
||||||
viewQueries: [],
|
viewQueries,
|
||||||
|
|
||||||
// These will be replaced during the compilation step, after all `NgModule`s have been
|
// These will be replaced during the compilation step, after all `NgModule`s have been
|
||||||
// analyzed and the full compilation scope for the component can be realized.
|
// analyzed and the full compilation scope for the component can be realized.
|
||||||
|
|
|
@ -6,11 +6,11 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler';
|
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
|
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
|
||||||
import {filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
|
import {Reference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
|
||||||
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
|
||||||
|
|
||||||
import {SelectorScopeRegistry} from './selector_scope';
|
import {SelectorScopeRegistry} from './selector_scope';
|
||||||
|
@ -29,8 +29,9 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
|
||||||
}
|
}
|
||||||
|
|
||||||
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
|
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
|
||||||
const analysis =
|
const directiveResult =
|
||||||
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
|
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
|
||||||
|
const analysis = directiveResult && directiveResult.metadata;
|
||||||
|
|
||||||
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
|
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
|
||||||
// when this directive appears in an `@NgModule` scope, its selector can be determined.
|
// when this directive appears in an `@NgModule` scope, its selector can be determined.
|
||||||
|
@ -58,7 +59,11 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
|
||||||
*/
|
*/
|
||||||
export function extractDirectiveMetadata(
|
export function extractDirectiveMetadata(
|
||||||
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker,
|
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker,
|
||||||
reflector: ReflectionHost, isCore: boolean): R3DirectiveMetadata|undefined {
|
reflector: ReflectionHost, isCore: boolean): {
|
||||||
|
decorator: Map<string, ts.Expression>,
|
||||||
|
metadata: R3DirectiveMetadata,
|
||||||
|
decoratedElements: ClassMember[],
|
||||||
|
}|undefined {
|
||||||
if (decorator.args === null || decorator.args.length !== 1) {
|
if (decorator.args === null || decorator.args.length !== 1) {
|
||||||
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
|
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
|
||||||
}
|
}
|
||||||
|
@ -93,6 +98,19 @@ export function extractDirectiveMetadata(
|
||||||
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
|
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
|
||||||
const outputsFromFields = parseDecoratedFields(
|
const outputsFromFields = parseDecoratedFields(
|
||||||
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), checker);
|
filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), checker);
|
||||||
|
// Construct the list of queries.
|
||||||
|
const contentChildFromFields = queriesFromFields(
|
||||||
|
filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), checker);
|
||||||
|
const contentChildrenFromFields = queriesFromFields(
|
||||||
|
filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), checker);
|
||||||
|
|
||||||
|
const queries = [...contentChildFromFields, ...contentChildrenFromFields];
|
||||||
|
|
||||||
|
if (directive.has('queries')) {
|
||||||
|
const queriesFromDecorator =
|
||||||
|
extractQueriesFromDecorator(directive.get('queries') !, reflector, checker, isCore);
|
||||||
|
queries.push(...queriesFromDecorator.content);
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the selector.
|
// Parse the selector.
|
||||||
let selector = '';
|
let selector = '';
|
||||||
|
@ -112,7 +130,7 @@ export function extractDirectiveMetadata(
|
||||||
// Detect if the component inherits from another class
|
// Detect if the component inherits from another class
|
||||||
const usesInheritance = clazz.heritageClauses !== undefined &&
|
const usesInheritance = clazz.heritageClauses !== undefined &&
|
||||||
clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
|
clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
|
||||||
return {
|
const metadata: R3DirectiveMetadata = {
|
||||||
name: clazz.name !.text,
|
name: clazz.name !.text,
|
||||||
deps: getConstructorDependencies(clazz, reflector, isCore),
|
deps: getConstructorDependencies(clazz, reflector, isCore),
|
||||||
host: {
|
host: {
|
||||||
|
@ -124,17 +142,105 @@ export function extractDirectiveMetadata(
|
||||||
usesOnChanges,
|
usesOnChanges,
|
||||||
},
|
},
|
||||||
inputs: {...inputsFromMeta, ...inputsFromFields},
|
inputs: {...inputsFromMeta, ...inputsFromFields},
|
||||||
outputs: {...outputsFromMeta, ...outputsFromFields},
|
outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector,
|
||||||
queries: [], selector,
|
|
||||||
type: new WrappedNodeExpr(clazz.name !),
|
type: new WrappedNodeExpr(clazz.name !),
|
||||||
typeSourceSpan: null !, usesInheritance,
|
typeSourceSpan: null !, usesInheritance,
|
||||||
};
|
};
|
||||||
|
return {decoratedElements, decorator: directive, metadata};
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertIsStringArray(value: any[]): value is string[] {
|
export function extractQueryMetadata(
|
||||||
|
name: string, args: ReadonlyArray<ts.Expression>, propertyName: string,
|
||||||
|
checker: ts.TypeChecker): R3QueryMetadata {
|
||||||
|
if (args.length === 0) {
|
||||||
|
throw new Error(`@${name} must have arguments`);
|
||||||
|
}
|
||||||
|
const first = name === 'ViewChild' || name === 'ContentChild';
|
||||||
|
const arg = staticallyResolve(args[0], checker);
|
||||||
|
|
||||||
|
// Extract the predicate
|
||||||
|
let predicate: Expression|string[]|null = null;
|
||||||
|
if (arg instanceof Reference) {
|
||||||
|
predicate = new WrappedNodeExpr(args[0]);
|
||||||
|
} else if (typeof arg === 'string') {
|
||||||
|
predicate = [arg];
|
||||||
|
} else if (isStringArrayOrDie(arg, '@' + name)) {
|
||||||
|
predicate = arg as string[];
|
||||||
|
} else {
|
||||||
|
throw new Error(`@${name} predicate cannot be interpreted`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the read and descendants options.
|
||||||
|
let read: Expression|null = null;
|
||||||
|
// The default value for descendants is true for every decorator except @ContentChildren.
|
||||||
|
let descendants: boolean = name !== 'ContentChildren';
|
||||||
|
if (args.length === 2) {
|
||||||
|
const optionsExpr = unwrapExpression(args[1]);
|
||||||
|
if (!ts.isObjectLiteralExpression(optionsExpr)) {
|
||||||
|
throw new Error(`@${name} options must be an object literal`);
|
||||||
|
}
|
||||||
|
const options = reflectObjectLiteral(optionsExpr);
|
||||||
|
if (options.has('read')) {
|
||||||
|
read = new WrappedNodeExpr(options.get('read') !);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.has('descendants')) {
|
||||||
|
const descendantsValue = staticallyResolve(options.get('descendants') !, checker);
|
||||||
|
if (typeof descendantsValue !== 'boolean') {
|
||||||
|
throw new Error(`@${name} options.descendants must be a boolean`);
|
||||||
|
}
|
||||||
|
descendants = descendantsValue;
|
||||||
|
}
|
||||||
|
} else if (args.length > 2) {
|
||||||
|
// Too many arguments.
|
||||||
|
throw new Error(`@${name} has too many arguments`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
propertyName, predicate, first, descendants, read,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractQueriesFromDecorator(
|
||||||
|
queryData: ts.Expression, reflector: ReflectionHost, checker: ts.TypeChecker,
|
||||||
|
isCore: boolean): {
|
||||||
|
content: R3QueryMetadata[],
|
||||||
|
view: R3QueryMetadata[],
|
||||||
|
} {
|
||||||
|
const content: R3QueryMetadata[] = [], view: R3QueryMetadata[] = [];
|
||||||
|
const expr = unwrapExpression(queryData);
|
||||||
|
if (!ts.isObjectLiteralExpression(queryData)) {
|
||||||
|
throw new Error(`queries metadata must be an object literal`);
|
||||||
|
}
|
||||||
|
reflectObjectLiteral(queryData).forEach((queryExpr, propertyName) => {
|
||||||
|
queryExpr = unwrapExpression(queryExpr);
|
||||||
|
if (!ts.isNewExpression(queryExpr) || !ts.isIdentifier(queryExpr.expression)) {
|
||||||
|
throw new Error(`query metadata must be an instance of a query type`);
|
||||||
|
}
|
||||||
|
const type = reflector.getImportOfIdentifier(queryExpr.expression);
|
||||||
|
if (type === null || (!isCore && type.from !== '@angular/core') ||
|
||||||
|
!QUERY_TYPES.has(type.name)) {
|
||||||
|
throw new Error(`query metadata must be an instance of a query type`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = extractQueryMetadata(type.name, queryExpr.arguments || [], propertyName, checker);
|
||||||
|
if (type.name.startsWith('Content')) {
|
||||||
|
content.push(query);
|
||||||
|
} else {
|
||||||
|
view.push(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {content, view};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringArrayOrDie(value: any, name: string): value is string[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (let i = 0; i < value.length; i++) {
|
||||||
if (typeof value[i] !== 'string') {
|
if (typeof value[i] !== 'string') {
|
||||||
throw new Error(`Failed to resolve @Directive.inputs[${i}] to a string`);
|
throw new Error(`Failed to resolve ${name}[${i}] to a string`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -153,7 +259,7 @@ function parseFieldToPropertyMapping(
|
||||||
|
|
||||||
// Resolve the field of interest from the directive metadata to a string[].
|
// Resolve the field of interest from the directive metadata to a string[].
|
||||||
const metaValues = staticallyResolve(directive.get(field) !, checker);
|
const metaValues = staticallyResolve(directive.get(field) !, checker);
|
||||||
if (!Array.isArray(metaValues) || !assertIsStringArray(metaValues)) {
|
if (!isStringArrayOrDie(metaValues, field)) {
|
||||||
throw new Error(`Failed to resolve @Directive.${field}`);
|
throw new Error(`Failed to resolve @Directive.${field}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,3 +305,29 @@ function parseDecoratedFields(
|
||||||
},
|
},
|
||||||
{} as{[field: string]: string});
|
{} as{[field: string]: string});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function queriesFromFields(
|
||||||
|
fields: {member: ClassMember, decorators: Decorator[]}[],
|
||||||
|
checker: ts.TypeChecker): R3QueryMetadata[] {
|
||||||
|
return fields.map(({member, decorators}) => {
|
||||||
|
if (decorators.length !== 1) {
|
||||||
|
throw new Error(`Cannot have multiple query decorators on the same class member`);
|
||||||
|
} else if (!isPropertyTypeMember(member)) {
|
||||||
|
throw new Error(`Query decorator must go on a property-type member`);
|
||||||
|
}
|
||||||
|
const decorator = decorators[0];
|
||||||
|
return extractQueryMetadata(decorator.name, decorator.args || [], member.name, checker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPropertyTypeMember(member: ClassMember): boolean {
|
||||||
|
return member.kind === ClassMemberKind.Getter || member.kind === ClassMemberKind.Setter ||
|
||||||
|
member.kind === ClassMemberKind.Property;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUERY_TYPES = new Set([
|
||||||
|
'ContentChild',
|
||||||
|
'ContentChildren',
|
||||||
|
'ViewChild',
|
||||||
|
'ViewChildren',
|
||||||
|
]);
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type FnWithArg<T> = (arg?: any) => T;
|
interface FnWithArg<T> {
|
||||||
|
(...args: any[]): T;
|
||||||
|
new (...args: any[]): T;
|
||||||
|
}
|
||||||
|
|
||||||
function callableClassDecorator(): FnWithArg<(clazz: any) => any> {
|
function callableClassDecorator(): FnWithArg<(clazz: any) => any> {
|
||||||
return null !;
|
return null !;
|
||||||
|
@ -16,6 +19,10 @@ function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> {
|
||||||
return null !;
|
return null !;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function callablePropDecorator(): FnWithArg<(a: any, b: any) => any> {
|
||||||
|
return null !;
|
||||||
|
}
|
||||||
|
|
||||||
export const Component = callableClassDecorator();
|
export const Component = callableClassDecorator();
|
||||||
export const Directive = callableClassDecorator();
|
export const Directive = callableClassDecorator();
|
||||||
export const Injectable = callableClassDecorator();
|
export const Injectable = callableClassDecorator();
|
||||||
|
@ -27,6 +34,12 @@ export const Inject = callableParamDecorator();
|
||||||
export const Self = callableParamDecorator();
|
export const Self = callableParamDecorator();
|
||||||
export const SkipSelf = callableParamDecorator();
|
export const SkipSelf = callableParamDecorator();
|
||||||
export const Optional = callableParamDecorator();
|
export const Optional = callableParamDecorator();
|
||||||
|
|
||||||
|
export const ContentChild = callablePropDecorator();
|
||||||
|
export const ContentChildren = callablePropDecorator();
|
||||||
|
export const ViewChild = callablePropDecorator();
|
||||||
|
export const ViewChildren = callablePropDecorator();
|
||||||
|
|
||||||
export type ModuleWithProviders<T> = any;
|
export type ModuleWithProviders<T> = any;
|
||||||
|
|
||||||
export class ChangeDetectorRef {}
|
export class ChangeDetectorRef {}
|
||||||
|
|
|
@ -412,4 +412,36 @@ describe('ngtsc behavioral tests', () => {
|
||||||
.toContain(
|
.toContain(
|
||||||
`factory: function FooCmp_Factory() { return new FooCmp(i0.ɵinjectAttribute("test"), i0.ɵinjectChangeDetectorRef(), i0.ɵinjectElementRef(), i0.ɵdirectiveInject(i0.INJECTOR), i0.ɵinjectTemplateRef(), i0.ɵinjectViewContainerRef()); }`);
|
`factory: function FooCmp_Factory() { return new FooCmp(i0.ɵinjectAttribute("test"), i0.ɵinjectChangeDetectorRef(), i0.ɵinjectElementRef(), i0.ɵdirectiveInject(i0.INJECTOR), i0.ɵinjectTemplateRef(), i0.ɵinjectViewContainerRef()); }`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate queries for components', () => {
|
||||||
|
writeConfig();
|
||||||
|
write(`test.ts`, `
|
||||||
|
import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'test',
|
||||||
|
template: '<div #foo></div>',
|
||||||
|
queries: {
|
||||||
|
'mview': new ViewChild('test1'),
|
||||||
|
'mcontent': new ContentChild('test2'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class FooCmp {
|
||||||
|
@ContentChild('bar', {read: TemplateRef}) child: any;
|
||||||
|
@ContentChildren(TemplateRef) children: any;
|
||||||
|
get aview(): any { return null; }
|
||||||
|
@ViewChild('accessor') set aview(value: any) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], errorSpy);
|
||||||
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const jsContents = getContents('test.js');
|
||||||
|
expect(jsContents).toContain(`i0.ɵQ(null, ["bar"], true, TemplateRef)`);
|
||||||
|
expect(jsContents).toContain(`i0.ɵQ(null, TemplateRef, false)`);
|
||||||
|
expect(jsContents).toContain(`i0.ɵQ(null, ["test2"], true)`);
|
||||||
|
expect(jsContents).toContain(`i0.ɵQ(0, ["accessor"], true)`);
|
||||||
|
expect(jsContents).toContain(`i0.ɵQ(1, ["test1"], true)`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue