From 053bf27fb31bf5a6bac3b0206735404f7c39a7ef Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Mon, 24 Sep 2018 10:30:29 +0200 Subject: [PATCH] refactor(ivy): use context discovery in TestBed implementation (#26211) PR Close #26211 --- integration/ng_elements/e2e/app.e2e-spec.ts | 4 +- packages/core/src/render3/debug.ts | 83 +++++++------------ packages/core/src/render3/discovery_utils.ts | 6 +- .../core/src/render3/node_manipulation.ts | 2 +- packages/core/src/view/services.ts | 15 ++-- packages/core/test/test_bed_spec.ts | 34 +++++++- 6 files changed, 76 insertions(+), 68 deletions(-) diff --git a/integration/ng_elements/e2e/app.e2e-spec.ts b/integration/ng_elements/e2e/app.e2e-spec.ts index cae28b48f5..3cf0e87759 100644 --- a/integration/ng_elements/e2e/app.e2e-spec.ts +++ b/integration/ng_elements/e2e/app.e2e-spec.ts @@ -13,8 +13,8 @@ describe('Element E2E Tests', function () { browser.get('hello-world.html'); const helloWorldEl = element(by.css('hello-world-el')); const input = element(by.css('input[type=text]')); - input.sendKeys('F', 'o', 'o'); - expect(helloWorldEl.getText()).toEqual('Hello Foo!'); + ['f', 'o', 'o'].forEach((key) => input.sendKeys(key)); + expect(helloWorldEl.getText()).toEqual('Hello foo!'); }); }); }); diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts index 8199947f95..968a46cdcf 100644 --- a/packages/core/src/render3/debug.ts +++ b/packages/core/src/render3/debug.ts @@ -11,11 +11,10 @@ import {Renderer2, RendererType2} from '../render/api'; import {DebugContext} from '../view'; import {DebugRenderer2, DebugRendererFactory2} from '../view/services'; -import * as di from './di'; -import {_getViewData} from './instructions'; -import {TNodeFlags} from './interfaces/node'; -import {CONTEXT, LViewData, TVIEW} from './interfaces/view'; - +import {getHostComponent, getInjector, getLocalRefs, loadContext} from './discovery_utils'; +import {DirectiveDef} from './interfaces/definition'; +import {TNode, TNodeFlags} from './interfaces/node'; +import {TVIEW} from './interfaces/view'; /** * Adapts the DebugRendererFactory2 to create a DebugRenderer2 specific for IVY. @@ -25,7 +24,7 @@ import {CONTEXT, LViewData, TVIEW} from './interfaces/view'; export class Render3DebugRendererFactory2 extends DebugRendererFactory2 { createRenderer(element: any, renderData: RendererType2|null): Renderer2 { const renderer = super.createRenderer(element, renderData) as DebugRenderer2; - renderer.debugContextFactory = () => new Render3DebugContext(_getViewData()); + renderer.debugContextFactory = (nativeElement: any) => new Render3DebugContext(nativeElement); return renderer; } } @@ -36,68 +35,46 @@ export class Render3DebugRendererFactory2 extends DebugRendererFactory2 { * Used in tests to retrieve information those nodes. */ class Render3DebugContext implements DebugContext { - readonly nodeIndex: number|null; + constructor(private _nativeNode: any) {} - constructor(private viewData: LViewData) { - // The LNode will be created next and appended to viewData - this.nodeIndex = viewData ? viewData.length : null; - } + get nodeIndex(): number|null { return loadContext(this._nativeNode).nodeIndex; } - get view(): any { return this.viewData; } + get view(): any { return loadContext(this._nativeNode).lViewData; } - get injector(): Injector { - if (this.nodeIndex !== null) { - const tNode = this.view[TVIEW].data[this.nodeIndex]; - return new di.NodeInjector(tNode, this.view); - } - return Injector.NULL; - } + get injector(): Injector { return getInjector(this._nativeNode); } - get component(): any { - // TODO(vicb): why/when - if (this.nodeIndex === null) { - return null; - } + get component(): any { return getHostComponent(this._nativeNode); } - const tView = this.view[TVIEW]; - const components: number[]|null = tView.components; - - return (components && components.indexOf(this.nodeIndex) == -1) ? - null : - this.view[this.nodeIndex].data[CONTEXT]; - } - - // TODO(vicb): add view providers when supported get providerTokens(): any[] { - // TODO(vicb): why/when - const directiveDefs = this.view[TVIEW].data; - if (this.nodeIndex === null || directiveDefs == null) { - return []; + const lDebugCtx = loadContext(this._nativeNode); + const lViewData = lDebugCtx.lViewData; + const tNode = lViewData[TVIEW].data[lDebugCtx.nodeIndex] as TNode; + const directivesCount = tNode.flags & TNodeFlags.DirectiveCountMask; + + if (directivesCount > 0) { + const directiveIdxStart = tNode.flags >> TNodeFlags.DirectiveStartingIndexShift; + const directiveIdxEnd = directiveIdxStart + directivesCount; + const viewDirectiveDefs = this.view[TVIEW].data; + const directiveDefs = + viewDirectiveDefs.slice(directiveIdxStart, directiveIdxEnd) as DirectiveDef[]; + + return directiveDefs.map(directiveDef => directiveDef.type); } - const currentTNode = this.view[TVIEW].data[this.nodeIndex]; - const dirStart = currentTNode >> TNodeFlags.DirectiveStartingIndexShift; - const dirEnd = dirStart + (currentTNode & TNodeFlags.DirectiveCountMask); - return directiveDefs.slice(dirStart, dirEnd); + return []; } - get references(): {[key: string]: any} { - // TODO(vicb): implement retrieving references - throw new Error('Not implemented yet in ivy'); - } + get references(): {[key: string]: any} { return getLocalRefs(this._nativeNode); } - get context(): any { - if (this.nodeIndex === null) { - return null; - } - const lNode = this.view[this.nodeIndex]; - return lNode.view[CONTEXT]; - } + // TODO(pk): check previous implementation and re-implement + get context(): any { throw new Error('Not implemented in ivy'); } + // TODO(pk): check previous implementation and re-implement get componentRenderElement(): any { throw new Error('Not implemented in ivy'); } + // TODO(pk): check previous implementation and re-implement get renderNode(): any { throw new Error('Not implemented in ivy'); } - // TODO(vicb): check previous implementation + // TODO(pk): check previous implementation and re-implement logError(console: Console, ...values: any[]): void { console.error(...values); } } diff --git a/packages/core/src/render3/discovery_utils.ts b/packages/core/src/render3/discovery_utils.ts index 8fd4e4bb86..4f8fceb4dc 100644 --- a/packages/core/src/render3/discovery_utils.ts +++ b/packages/core/src/render3/discovery_utils.ts @@ -110,7 +110,11 @@ export function getDirectives(target: {}): Array<{}> { return context.directives || []; } -function loadContext(target: {}): LContext { +/** + * Returns LContext associated with a target passed as an argument. + * Throws if a given target doesn't have associated LContext. + */ +export function loadContext(target: {}): LContext { const context = getContext(target); if (!context) { throw new Error( diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 607cc163ab..1a518f4b42 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -395,8 +395,8 @@ export function detachView(lContainer: LContainer, removeIndex: number, detached export function removeView( lContainer: LContainer, tContainer: TContainerNode, removeIndex: number) { const view = lContainer[VIEWS][removeIndex]; - destroyLView(view); detachView(lContainer, removeIndex, !!tContainer.detached); + destroyLView(view); } /** Gets the child of the given LViewData */ diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index c3766184e8..0af52ade52 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -12,6 +12,7 @@ import {InjectableDef, getInjectableDef} from '../di/defs'; import {InjectableType} from '../di/injectable'; import {ErrorHandler} from '../error_handler'; import {isDevMode} from '../is_dev_mode'; +import {ivyEnabled} from '../ivy_switch/compiler/index'; import {ComponentFactory} from '../linker/component_factory'; import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; @@ -694,6 +695,8 @@ export class DebugRendererFactory2 implements RendererFactory2 { export class DebugRenderer2 implements Renderer2 { readonly data: {[key: string]: any}; + private createDebugContext(nativeElement: any) { return this.debugContextFactory(nativeElement); } + /** * Factory function used to create a `DebugContext` when a node is created. * @@ -702,9 +705,7 @@ export class DebugRenderer2 implements Renderer2 { * The factory is configurable so that the `DebugRenderer2` could instantiate either a View Engine * or a Render context. */ - debugContextFactory: () => DebugContext | null = getCurrentDebugContext; - - private get debugContext() { return this.debugContextFactory(); } + debugContextFactory: (nativeElement?: any) => DebugContext | null = getCurrentDebugContext; constructor(private delegate: Renderer2) { this.data = this.delegate.data; } @@ -719,7 +720,7 @@ export class DebugRenderer2 implements Renderer2 { createElement(name: string, namespace?: string): any { const el = this.delegate.createElement(name, namespace); - const debugCtx = this.debugContext; + const debugCtx = this.createDebugContext(el); if (debugCtx) { const debugEl = new DebugElement(el, null, debugCtx); debugEl.name = name; @@ -730,7 +731,7 @@ export class DebugRenderer2 implements Renderer2 { createComment(value: string): any { const comment = this.delegate.createComment(value); - const debugCtx = this.debugContext; + const debugCtx = this.createDebugContext(comment); if (debugCtx) { indexDebugNode(new DebugNode(comment, null, debugCtx)); } @@ -739,7 +740,7 @@ export class DebugRenderer2 implements Renderer2 { createText(value: string): any { const text = this.delegate.createText(value); - const debugCtx = this.debugContext; + const debugCtx = this.createDebugContext(text); if (debugCtx) { indexDebugNode(new DebugNode(text, null, debugCtx)); } @@ -777,7 +778,7 @@ export class DebugRenderer2 implements Renderer2 { selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any { const el = this.delegate.selectRootElement(selectorOrNode, preserveContent); - const debugCtx = getCurrentDebugContext(); + const debugCtx = getCurrentDebugContext() || (ivyEnabled ? this.createDebugContext(el) : null); if (debugCtx) { indexDebugNode(new DebugElement(el, null, debugCtx)); } diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index fa6671668b..666617c6f6 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -43,8 +43,12 @@ export class GreetingModule { export class SimpleCmp { } +@Component({selector: 'with-refs-cmp', template: '
'}) +export class WithRefsCmp { +} + @NgModule({ - declarations: [HelloWorld, SimpleCmp], + declarations: [HelloWorld, SimpleCmp, WithRefsCmp], imports: [GreetingModule], providers: [ {provide: NAME, useValue: 'World!'}, @@ -94,11 +98,27 @@ describe('TestBed', () => { expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!'); }); - it('should give access to the node injector', () => { + const fixture = TestBed.createComponent(HelloWorld); + fixture.detectChanges(); + const injector = fixture.debugElement.query(By.css('greeting-cmp')).injector; + + // from the node injector + const greetingCmp = injector.get(GreetingCmp); + expect(greetingCmp.constructor).toBe(GreetingCmp); + + // from the node injector (inherited from a parent node) + const helloWorldCmp = injector.get(HelloWorld); + expect(fixture.componentInstance).toBe(helloWorldCmp); + + const nameInjected = injector.get(NAME); + expect(nameInjected).toEqual('World!'); + }); + + it('should give access to the node injector for root node', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); - const injector = hello.debugElement.query(By.css('greeting-cmp')).injector; + const injector = hello.debugElement.injector; // from the node injector const helloInjected = injector.get(HelloWorld); @@ -109,6 +129,13 @@ describe('TestBed', () => { expect(nameInjected).toEqual('World!'); }); + it('should give access to local refs on a node', () => { + const withRefsCmp = TestBed.createComponent(WithRefsCmp); + const firstDivDebugEl = withRefsCmp.debugElement.query(By.css('div')); + // assert that a native element is referenced by a local ref + expect(firstDivDebugEl.references.firstDiv.tagName.toLowerCase()).toBe('div'); + }); + it('should give the ability to query by directive', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); @@ -117,7 +144,6 @@ describe('TestBed', () => { expect(greetingByDirective.componentInstance).toBeAnInstanceOf(GreetingCmp); }); - it('allow to override a template', () => { // use original template when there is no override let hello = TestBed.createComponent(HelloWorld);