From 3fe21a67ca78b04b4c9ab68fa070726e5d60e14b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 20 Sep 2022 10:07:24 +0200 Subject: [PATCH] refactor(core): expose host directives to their host through DI (#47476) Exposes the host directives to the host and its descendants through DI. This can be useful, because it allows the host to further configure the host directives. PR Close #47476 --- .../core/src/render3/context_discovery.ts | 13 +- .../features/host_directives_feature.ts | 4 +- .../core/src/render3/instructions/shared.ts | 79 +-- .../core/src/render3/util/discovery_utils.ts | 2 +- .../test/acceptance/host_directives_spec.ts | 469 ++++++++++++++++-- 5 files changed, 466 insertions(+), 101 deletions(-) diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index e97c2639f1f51..74d6e81ecff39 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -63,7 +63,7 @@ export function getLContext(target: any): LContext|null { if (nodeIndex == -1) { throw new Error('The provided directive was not found in the application'); } - directives = getDirectivesAtNodeIndex(nodeIndex, lView, false); + directives = getDirectivesAtNodeIndex(nodeIndex, lView); } else { nodeIndex = findViaNativeElement(lView, target as RElement); if (nodeIndex == -1) { @@ -294,21 +294,20 @@ function findViaDirective(lView: LView, directiveInstance: {}): number { } /** - * Returns a list of directives extracted from the given view based on the - * provided list of directive index values. + * Returns a list of directives applied to a node at a specific index. The list includes + * directives matched by selector and any host directives, but it excludes components. + * Use `getComponentAtNodeIndex` to find the component applied to a node. * * @param nodeIndex The node index * @param lView The target view data - * @param includeComponents Whether or not to include components in returned directives */ -export function getDirectivesAtNodeIndex( - nodeIndex: number, lView: LView, includeComponents: boolean): any[]|null { +export function getDirectivesAtNodeIndex(nodeIndex: number, lView: LView): any[]|null { const tNode = lView[TVIEW].data[nodeIndex] as TNode; if (tNode.directiveStart === 0) return EMPTY_ARRAY; const results: any[] = []; for (let i = tNode.directiveStart; i < tNode.directiveEnd; i++) { const directiveInstance = lView[i]; - if (!isComponentInstance(directiveInstance) || includeComponents) { + if (!isComponentInstance(directiveInstance)) { results.push(directiveInstance); } } diff --git a/packages/core/src/render3/features/host_directives_feature.ts b/packages/core/src/render3/features/host_directives_feature.ts index ccb360f01b5d1..16d159f05dbcd 100644 --- a/packages/core/src/render3/features/host_directives_feature.ts +++ b/packages/core/src/render3/features/host_directives_feature.ts @@ -68,11 +68,9 @@ function findHostDirectiveDefs( // Host directives execute before the host so that its host bindings can be overwritten. findHostDirectiveDefs(matches, hostDirectiveDef, tView, lView, tNode); + matches.push(hostDirectiveDef); } } - - // Push the def itself at the end since it needs to execute after the host directives. - matches.push(def); } /** diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index b81acd3464dc2..1f21e991da699 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -1065,13 +1065,17 @@ export function resolveDirectives( let hasDirectives = false; if (getBindingsEnabled()) { - const directiveDefsMatchedBySelectors = findDirectiveDefMatches(tView, lView, tNode); - const directiveDefs = directiveDefsMatchedBySelectors ? - findHostDirectiveDefs(directiveDefsMatchedBySelectors, tView, lView, tNode) : - null; + const directiveDefs = findDirectiveDefMatches(tView, lView, tNode); const exportsMap: ({[key: string]: number}|null) = localRefs === null ? null : {'': -1}; if (directiveDefs !== null) { + // Publishes the directive types to DI so they can be injected. Needs to + // happen in a separate pass before the TNode flags have been initialized. + for (let i = 0; i < directiveDefs.length; i++) { + diPublicInInjector( + getOrCreateNodeInjectorForNode(tNode, lView), tView, directiveDefs[i].type); + } + hasDirectives = true; initTNodeFlags(tNode, tView.data.length, directiveDefs.length); // When the same token is provided by several directives on the same node, some rules apply in @@ -1261,19 +1265,18 @@ export function invokeHostBindingsInCreationMode(def: DirectiveDef, directi * If a component is matched (at most one), it is returned in first position in the array. */ function findDirectiveDefMatches( - tView: TView, viewData: LView, - tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef[]|null { + tView: TView, lView: LView, + tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef[]|null { ngDevMode && assertFirstCreatePass(tView); ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer); const registry = tView.directiveRegistry; - let matches: any[]|null = null; + let matches: DirectiveDef[]|null = null; if (registry) { for (let i = 0; i < registry.length; i++) { const def = registry[i] as ComponentDef| DirectiveDef; if (isNodeMatchingSelectorList(tNode, def.selectors!, /* isProjectionMode */ false)) { matches || (matches = ngDevMode ? new MatchesArray() : []); - diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type); if (isComponentDef(def)) { if (ngDevMode) { @@ -1283,15 +1286,39 @@ function findDirectiveDefMatches( `Please use a different tag to activate the ${stringify(def.type)} component.`); if (isComponentHost(tNode)) { - // If another component has been matched previously, it's the first element in the - // `matches` array, see how we store components/directives in `matches` below. - throwMultipleComponentError(tNode, matches[0].type, def.type); + throwMultipleComponentError(tNode, matches.find(isComponentDef)!.type, def.type); } } - markAsComponentHost(tView, tNode, 0); - // The component is always stored first with directives after. - matches.unshift(def); + + // Components are inserted at the front of the matches array so that their lifecycle + // hooks run before any directive lifecycle hooks. This appears to be for ViewEngine + // compatibility. This logic doesn't make sense with host directives, because it + // would allow the host directives to undo any overrides the host may have made. + // To handle this case, the host directives of components are inserted at the beginning + // of the array, followed by the component. As such, the insertion order is as follows: + // 1. Host directives belonging to the selector-matched component. + // 2. Selector-matched component. + // 3. Host directives belonging to selector-matched directives. + // 4. Selector-matched directives. + if (def.findHostDirectiveDefs !== null) { + const hostDirectiveMatches: DirectiveDef[] = []; + def.findHostDirectiveDefs(hostDirectiveMatches, def, tView, lView, tNode); + // Add all host directives declared on this component, followed by the component itself. + // Host directives should execute first so the host has a chance to override changes + // to the DOM made by them. + matches.unshift(...hostDirectiveMatches, def); + // Component is offset starting from the beginning of the host directives array. + const componentOffset = hostDirectiveMatches.length; + markAsComponentHost(tView, tNode, componentOffset); + } else { + // No host directives on this component, just add the + // component def to the beginning of the matches. + matches.unshift(def); + markAsComponentHost(tView, tNode, 0); + } } else { + // Append any host directives to the matches first. + def.findHostDirectiveDefs?.(matches, def, tView, lView, tNode); matches.push(def); } } @@ -1313,30 +1340,6 @@ export function markAsComponentHost(tView: TView, hostTNode: TNode, componentOff .push(hostTNode.index); } -/** - * Given an array of directives that were matched by their selectors, this function - * produces a new array that also includes any host directives that have to be applied. - * @param selectorMatches Directives matched in a template based on their selectors. - * @param tView Current TView. - * @param lView Current LView. - * @param tNode Current TNode that is being matched. - */ -function findHostDirectiveDefs( - selectorMatches: DirectiveDef[], tView: TView, lView: LView, - tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef[] { - const matches: DirectiveDef[] = []; - - for (const def of selectorMatches) { - if (def.findHostDirectiveDefs === null) { - matches.push(def); - } else { - def.findHostDirectiveDefs(matches, def, tView, lView, tNode); - } - } - - return matches; -} - /** Caches local names and their matching directive indices for query and template lookups. */ function cacheMatchingLocalNames( tNode: TNode, localRefs: string[]|null, exportsMap: {[key: string]: number}): void { diff --git a/packages/core/src/render3/util/discovery_utils.ts b/packages/core/src/render3/util/discovery_utils.ts index ac24234817a8a..2a910ccde8982 100644 --- a/packages/core/src/render3/util/discovery_utils.ts +++ b/packages/core/src/render3/util/discovery_utils.ts @@ -221,7 +221,7 @@ export function getDirectives(node: Node): {}[] { return []; } if (context.directives === undefined) { - context.directives = getDirectivesAtNodeIndex(nodeIndex, lView, false); + context.directives = getDirectivesAtNodeIndex(nodeIndex, lView); } // The `directives` in this case are a named array called `LComponentView`. Clone the diff --git a/packages/core/test/acceptance/host_directives_spec.ts b/packages/core/test/acceptance/host_directives_spec.ts index a8bca6071a5b7..ac21077977277 100644 --- a/packages/core/test/acceptance/host_directives_spec.ts +++ b/packages/core/test/acceptance/host_directives_spec.ts @@ -6,8 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterViewChecked, AfterViewInit, Component, Directive, forwardRef, inject, Inject, InjectionToken, OnInit, ViewChild} from '@angular/core'; +import {AfterViewChecked, AfterViewInit, Component, Directive, ElementRef, forwardRef, inject, Inject, InjectionToken, Input, OnInit, ViewChild} from '@angular/core'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +import {getComponent, getDirectives} from '../../src/render3/util/discovery_utils'; /** * Temporary `any` used for metadata until `hostDirectives` is enabled publicly. @@ -183,20 +186,38 @@ describe('host directives', () => { } as HostDirectiveAny) class MyComp { constructor() { - logs.push('host'); + logs.push('MyComp'); + } + } + + @Directive({standalone: true}) + class SelectorMatchedHostDir { + constructor() { + logs.push('SelectorMatchedHostDir'); + } + } + + @Directive({ + selector: '[selector-matched-dir]', + hostDirectives: [SelectorMatchedHostDir], + } as HostDirectiveAny) + class SelectorMatchedDir { + constructor() { + logs.push('SelectorMatchedDir'); } } - @Component({template: ''}) + + @Component({template: ''}) class App { } - TestBed.configureTestingModule({declarations: [App, MyComp]}); + TestBed.configureTestingModule({declarations: [App, MyComp, SelectorMatchedDir]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(diTokenValue!).toBe('host value'); expect(fixture.nativeElement.innerHTML) - .toBe(''); + .toBe(''); expect(logs).toEqual([ 'Chain1 - level 3', 'Chain1 - level 2', @@ -205,7 +226,9 @@ describe('host directives', () => { 'Chain2 - level 1', 'Chain3 - level 2', 'Chain3 - level 1', - 'host', + 'MyComp', + 'SelectorMatchedHostDir', + 'SelectorMatchedDir', ]); }); @@ -288,73 +311,166 @@ describe('host directives', () => { expect(fixture.nativeElement.textContent).toContain('FirstHost | SecondHost'); }); - it('should invoke lifecycle hooks from the host directives', () => { - const logs: string[] = []; + // TODO(crisbeto): add a test for `ngOnChanges`. + describe('lifecycle hooks', () => { + it('should invoke lifecycle hooks from the host directives', () => { + const logs: string[] = []; - @Directive({standalone: true}) - class HostDir implements OnInit, AfterViewInit, AfterViewChecked { - ngOnInit() { - logs.push('HostDir - ngOnInit'); + @Directive({standalone: true}) + class HostDir implements OnInit, AfterViewInit, AfterViewChecked { + ngOnInit() { + logs.push('HostDir - ngOnInit'); + } + + ngAfterViewInit() { + logs.push('HostDir - ngAfterViewInit'); + } + + ngAfterViewChecked() { + logs.push('HostDir - ngAfterViewChecked'); + } } - ngAfterViewInit() { - logs.push('HostDir - ngAfterViewInit'); + @Directive({standalone: true}) + class OtherHostDir implements OnInit, AfterViewInit, AfterViewChecked { + ngOnInit() { + logs.push('OtherHostDir - ngOnInit'); + } + + ngAfterViewInit() { + logs.push('OtherHostDir - ngAfterViewInit'); + } + + ngAfterViewChecked() { + logs.push('OtherHostDir - ngAfterViewChecked'); + } } - ngAfterViewChecked() { - logs.push('HostDir - ngAfterViewChecked'); + @Directive({selector: '[dir]', hostDirectives: [HostDir, OtherHostDir]} as HostDirectiveAny) + class Dir implements OnInit, AfterViewInit, AfterViewChecked { + ngOnInit() { + logs.push('Dir - ngOnInit'); + } + + ngAfterViewInit() { + logs.push('Dir - ngAfterViewInit'); + } + + ngAfterViewChecked() { + logs.push('Dir - ngAfterViewChecked'); + } } - } - @Directive({standalone: true}) - class OtherHostDir implements OnInit, AfterViewInit, AfterViewChecked { - ngOnInit() { - logs.push('OtherHostDir - ngOnInit'); + @Component({template: '
'}) + class App { } - ngAfterViewInit() { - logs.push('OtherHostDir - ngAfterViewInit'); + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(logs).toEqual([ + 'HostDir - ngOnInit', + 'OtherHostDir - ngOnInit', + 'Dir - ngOnInit', + 'HostDir - ngAfterViewInit', + 'HostDir - ngAfterViewChecked', + 'OtherHostDir - ngAfterViewInit', + 'OtherHostDir - ngAfterViewChecked', + 'Dir - ngAfterViewInit', + 'Dir - ngAfterViewChecked', + ]); + }); + + // Note: lifecycle hook order is different when components and directives are mixed so this + // test aims to cover it. Usually lifecycle hooks are invoked based on the order in which + // directives were matched, but components bypass this logic and always execute first. + it('should invoke host directive lifecycle hooks before the host component hooks', () => { + const logs: string[] = []; + + // Utility so we don't have to repeat the logging code. + @Directive({standalone: true}) + abstract class LogsLifecycles implements OnInit, AfterViewInit { + abstract name: string; + + ngOnInit() { + logs.push(`${this.name} - ngOnInit`); + } + + ngAfterViewInit() { + logs.push(`${this.name} - ngAfterViewInit`); + } } - ngAfterViewChecked() { - logs.push('OtherHostDir - ngAfterViewChecked'); + @Directive({standalone: true}) + class ChildHostDir extends LogsLifecycles { + override name = 'ChildHostDir'; } - } - @Directive({selector: '[dir]', hostDirectives: [HostDir, OtherHostDir]} as HostDirectiveAny) - class Dir implements OnInit, AfterViewInit, AfterViewChecked { - ngOnInit() { - logs.push('Dir - ngOnInit'); + @Directive({standalone: true}) + class OtherChildHostDir extends LogsLifecycles { + override name = 'OtherChildHostDir'; } - ngAfterViewInit() { - logs.push('Dir - ngAfterViewInit'); + @Component({ + selector: 'child', + hostDirectives: [ChildHostDir, OtherChildHostDir], + } as HostDirectiveAny) + class Child extends LogsLifecycles { + override name = 'Child'; } - ngAfterViewChecked() { - logs.push('Dir - ngAfterViewChecked'); + @Directive({standalone: true}) + class ParentHostDir extends LogsLifecycles { + override name = 'ParentHostDir'; } - } - @Component({template: '
'}) - class App { - } + @Directive({standalone: true}) + class OtherParentHostDir extends LogsLifecycles { + override name = 'OtherParentHostDir'; + } - TestBed.configureTestingModule({declarations: [App, Dir]}); - const fixture = TestBed.createComponent(App); - fixture.detectChanges(); + @Component({ + selector: 'parent', + hostDirectives: [ParentHostDir, OtherParentHostDir], + template: '', + } as HostDirectiveAny) + class Parent extends LogsLifecycles { + override name = 'Parent'; + } - expect(logs).toEqual([ - 'HostDir - ngOnInit', - 'OtherHostDir - ngOnInit', - 'Dir - ngOnInit', - 'HostDir - ngAfterViewInit', - 'HostDir - ngAfterViewChecked', - 'OtherHostDir - ngAfterViewInit', - 'OtherHostDir - ngAfterViewChecked', - 'Dir - ngAfterViewInit', - 'Dir - ngAfterViewChecked', - ]); + @Directive({selector: '[plain-dir]'}) + class PlainDir extends LogsLifecycles { + @Input('plain-dir') override name = ''; + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Parent, Child, PlainDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(logs).toEqual([ + 'ParentHostDir - ngOnInit', + 'OtherParentHostDir - ngOnInit', + 'Parent - ngOnInit', + 'PlainDir on parent - ngOnInit', + 'ChildHostDir - ngOnInit', + 'OtherChildHostDir - ngOnInit', + 'Child - ngOnInit', + 'PlainDir on child - ngOnInit', + 'ChildHostDir - ngAfterViewInit', + 'OtherChildHostDir - ngAfterViewInit', + 'Child - ngAfterViewInit', + 'PlainDir on child - ngAfterViewInit', + 'ParentHostDir - ngAfterViewInit', + 'OtherParentHostDir - ngAfterViewInit', + 'Parent - ngAfterViewInit', + 'PlainDir on parent - ngAfterViewInit', + ]); + }); }); describe('host bindings', () => { @@ -436,6 +552,89 @@ describe('host directives', () => { }); describe('dependency injection', () => { + it('should allow the host to inject its host directives', () => { + let hostInstance!: Host; + let firstHostDirInstance!: FirstHostDir; + let secondHostDirInstance!: SecondHostDir; + + @Directive({standalone: true}) + class SecondHostDir { + constructor() { + secondHostDirInstance = this; + } + } + + @Directive({standalone: true, hostDirectives: [SecondHostDir]} as HostDirectiveAny) + class FirstHostDir { + constructor() { + firstHostDirInstance = this; + } + } + + @Directive({selector: '[dir]', hostDirectives: [FirstHostDir]} as HostDirectiveAny) + class Host { + firstHostDir = inject(FirstHostDir); + secondHostDir = inject(SecondHostDir); + + constructor() { + hostInstance = this; + } + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(hostInstance instanceof Host).toBe(true); + expect(firstHostDirInstance instanceof FirstHostDir).toBe(true); + expect(secondHostDirInstance instanceof SecondHostDir).toBe(true); + + expect(hostInstance.firstHostDir).toBe(firstHostDirInstance); + expect(hostInstance.secondHostDir).toBe(secondHostDirInstance); + }); + + it('should be able to inject a host directive into a child component', () => { + let hostDirectiveInstance!: HostDir; + + @Component({selector: 'child', template: ''}) + class Child { + hostDir = inject(HostDir); + } + + @Directive({standalone: true}) + class HostDir { + constructor() { + hostDirectiveInstance = this; + } + } + + @Component({ + selector: 'host', + template: '', + hostDirectives: [HostDir], + } as HostDirectiveAny) + class Host { + @ViewChild(Child) child!: Child; + } + + @Component({template: ''}) + class App { + @ViewChild(Host) host!: Host; + } + + TestBed.configureTestingModule({declarations: [App, Host, Child]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const injectedInstance = fixture.componentInstance.host.child.hostDir; + + expect(injectedInstance instanceof HostDir).toBe(true); + expect(injectedInstance).toBe(hostDirectiveInstance); + }); + it('should allow the host directives to inject their host', () => { let hostInstance!: Host; let firstHostDirInstance!: FirstHostDir; @@ -642,5 +841,171 @@ describe('host directives', () => { expect(tokenValue).toBe(null); }); + + it('should throw a circular dependency error if a host and a host directive inject each other', + () => { + @Directive({standalone: true}) + class HostDir { + host = inject(Host); + } + + @Directive({selector: '[dir]', hostDirectives: [HostDir]} as HostDirectiveAny) + class Host { + hostDir = inject(HostDir); + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + expect(() => TestBed.createComponent(App)) + .toThrowError(/NG0200: Circular dependency in DI detected for HostDir/); + }); + }); + + describe('debugging and testing utilities', () => { + it('should be able to retrieve host directives using ng.getDirectives', () => { + let hostDirInstance!: HostDir; + let otherHostDirInstance!: OtherHostDir; + let plainDirInstance!: PlainDir; + + @Directive({standalone: true}) + class HostDir { + constructor() { + hostDirInstance = this; + } + } + + @Directive({standalone: true}) + class OtherHostDir { + constructor() { + otherHostDirInstance = this; + } + } + + @Directive({selector: '[plain-dir]'}) + class PlainDir { + constructor() { + plainDirInstance = this; + } + } + + @Component({ + selector: 'comp', + template: '', + hostDirectives: [HostDir, OtherHostDir], + } as HostDirectiveAny) + class Comp { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Comp, PlainDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const componentHost = fixture.nativeElement.querySelector('comp'); + + expect(hostDirInstance instanceof HostDir).toBe(true); + expect(otherHostDirInstance instanceof OtherHostDir).toBe(true); + expect(plainDirInstance instanceof PlainDir).toBe(true); + expect(getDirectives(componentHost)).toEqual([ + hostDirInstance, otherHostDirInstance, plainDirInstance + ]); + }); + + it('should be able to retrieve components that have host directives using ng.getComponent', + () => { + let compInstance!: Comp; + + @Directive({standalone: true}) + class HostDir { + } + + @Component({ + selector: 'comp', + template: '', + hostDirectives: [HostDir], + } as HostDirectiveAny) + class Comp { + constructor() { + compInstance = this; + } + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Comp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const componentHost = fixture.nativeElement.querySelector('comp'); + + expect(compInstance instanceof Comp).toBe(true); + expect(getComponent(componentHost)).toBe(compInstance); + }); + + it('should be able to retrieve components that have host directives using DebugNode.componentInstance', + () => { + let compInstance!: Comp; + + @Directive({standalone: true}) + class HostDir { + } + + @Component({ + selector: 'comp', + template: '', + hostDirectives: [HostDir], + } as HostDirectiveAny) + class Comp { + constructor() { + compInstance = this; + } + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Comp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const node = fixture.debugElement.query(By.css('comp')); + + expect(compInstance instanceof Comp).toBe(true); + expect(node.componentInstance).toBe(compInstance); + }); + + it('should be able to query by a host directive', () => { + @Directive({standalone: true}) + class HostDir { + } + + @Component({ + selector: 'comp', + template: '', + hostDirectives: [HostDir], + } as HostDirectiveAny) + class Comp { + constructor(public elementRef: ElementRef) {} + } + + @Component({template: ''}) + class App { + @ViewChild(Comp) compInstance!: Comp; + } + + TestBed.configureTestingModule({declarations: [App, Comp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const expected = fixture.componentInstance.compInstance.elementRef.nativeElement; + const result = fixture.debugElement.query(By.directive(HostDir)).nativeElement; + + expect(result).toBe(expected); + }); }); });