From 4feb9b1c72209628a8a8fcad22fd730ecddee126 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Sat, 11 May 2019 14:48:44 +0300 Subject: [PATCH 001/275] build: ignore `.devcontainer/` directory (#30417) This makes it easier to experiment with VSCode's [remote development using docker containers][1] feature. In the future, we may check in the necessary files for users to use this feature, but for now ignoring the directory makes it easier play around and evaluate the feature. [1]: https://code.visualstudio.com/docs/remote/containers PR Close #30417 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fc7de6bcee..d7d46f27fd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ tools/gulp-tasks/cldr/cldr-data/ pubspec.lock .c9 .idea/ +.devcontainer .settings/ .vscode/launch.json .vscode/settings.json From 0d4a0b6519aee94f384622e276be61cb7037b879 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Sat, 11 May 2019 09:53:41 +0000 Subject: [PATCH 002/275] build(docs-infra): upgrade cli command docs sources to 73d875a70 (#30414) Updating [angular#master](https://github.com/angular/angular/tree/master) from [cli-builds#master](https://github.com/angular/cli-builds/tree/master). ## Relevant changes in [commit range](https://github.com/angular/cli-builds/compare/8dd9b98ac...73d875a70): **Modified** - help/build.json - help/e2e.json - help/serve.json - help/test.json - help/xi18n.json ## PR Close #30414 --- aio/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aio/package.json b/aio/package.json index 8f4a920b90..d09bd5c267 100644 --- a/aio/package.json +++ b/aio/package.json @@ -19,7 +19,7 @@ "build-local": "yarn ~~build", "prebuild-with-ivy": "yarn setup-local && node scripts/switch-to-ivy", "build-with-ivy": "yarn ~~build", - "extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 8dd9b98ac", + "extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 73d875a70", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint", "test": "yarn check-env && ng test", "pree2e": "yarn check-env && yarn update-webdriver", From 877b2285f982b6c7f133b722791357548821f7b1 Mon Sep 17 00:00:00 2001 From: D3T0N8R Date: Fri, 10 May 2019 09:27:49 -0400 Subject: [PATCH 003/275] docs: change reference to jQuery programmers (#30386) The section on Data Binding makes a reference to "any experienced jQuery programmer" which is a bit too narrow since there are also programmers that write their front end in pure JavaScript. PR Close #30386 --- aio/content/guide/architecture-components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aio/content/guide/architecture-components.md b/aio/content/guide/architecture-components.md index 75b1029fbd..c140abd4e9 100644 --- a/aio/content/guide/architecture-components.md +++ b/aio/content/guide/architecture-components.md @@ -75,7 +75,7 @@ Notice how custom components like this mix seamlessly with native HTML in the sa ### Data binding -Without a framework, you would be responsible for pushing data values into the HTML controls and turning user responses into actions and value updates. Writing such push and pull logic by hand is tedious, error-prone, and a nightmare to read, as any experienced jQuery programmer can attest. +Without a framework, you would be responsible for pushing data values into the HTML controls and turning user responses into actions and value updates. Writing such push and pull logic by hand is tedious, error-prone, and a nightmare to read, as any experienced front-end JavaScript programmer can attest. Angular supports *two-way data binding*, a mechanism for coordinating the parts of a template with the parts of a component. Add binding markup to the template HTML to tell Angular how to connect both sides. From 0cf09fc981cf90b90a5b715db13f089f08123969 Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Mon, 6 May 2019 15:33:34 +0200 Subject: [PATCH 004/275] test(ivy): use `TestBed` for render3 i18n tests (#30352) PR Close #30352 --- packages/core/test/acceptance/i18n_spec.ts | 964 ++++++++++++ packages/core/test/i18n_integration_spec.ts | 640 -------- packages/core/test/render3/i18n_spec.ts | 1471 +------------------ 3 files changed, 969 insertions(+), 2106 deletions(-) create mode 100644 packages/core/test/acceptance/i18n_spec.ts delete mode 100644 packages/core/test/i18n_integration_spec.ts diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts new file mode 100644 index 0000000000..f63ed86800 --- /dev/null +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -0,0 +1,964 @@ +/** + * @license + * Copyright Google Inc. 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 {Component, ContentChild, ContentChildren, Directive, HostBinding, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; + + +onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { + beforeEach(() => { + TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef]}); + }); + + it('should translate text', () => { + ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); + const fixture = initWithTemplate(AppComp, `
text
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
texte
`); + }); + + it('should support interpolations', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}!': 'Bonjour {$interpolation}!'}}); + const fixture = initWithTemplate(AppComp, `
Hello {{name}}!
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
Bonjour Angular!
`); + fixture.componentRef.instance.name = `John`; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
Bonjour John!
`); + }); + + it('should support interpolations with custom interpolation config', () => { + ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const interpolation = ['{%', '%}'] as[string, string]; + TestBed.overrideComponent(AppComp, {set: {interpolation}}); + const fixture = initWithTemplate(AppComp, `
Hello {% name %}
`); + + expect(fixture.nativeElement.innerHTML).toBe('
Bonjour Angular
'); + }); + + it('should support interpolations with complex expressions', () => { + ɵi18nConfigureLocalize({ + translations: + {'{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)'} + }); + const fixture = + initWithTemplate(AppComp, `
{{ name | uppercase }} - {{ obj?.a?.b }}
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
ANGULAR - (fr)
`); + fixture.componentRef.instance.obj = {a: {b: 'value'}}; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
ANGULAR - value (fr)
`); + }); + + it('should support elements', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Hello {$startTagSpan}world{$closeTagSpan} and {$startTagDiv}universe{$closeTagDiv}!': + 'Bonjour {$startTagSpan}monde{$closeTagSpan} et {$startTagDiv}univers{$closeTagDiv}!' + } + }); + const fixture = initWithTemplate( + AppComp, `
Hello world and
universe
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Bonjour monde et
univers
!
`); + }); + + it('should support removing elements', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Hello {$startBoldText}my{$closeBoldText}{$startTagSpan}world{$closeTagSpan}': + 'Bonjour {$startTagSpan}monde{$closeTagSpan}' + } + }); + const fixture = + initWithTemplate(AppComp, `
Hello myworld
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Bonjour monde
!
`); + }); + + it('should support moving elements', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Hello {$startTagSpan}world{$closeTagSpan} and {$startTagDiv}universe{$closeTagDiv}!': + 'Bonjour {$startTagDiv}univers{$closeTagDiv} et {$startTagSpan}monde{$closeTagSpan}!' + } + }); + const fixture = initWithTemplate( + AppComp, `
Hello world and
universe
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Bonjour
univers
et monde!
`); + }); + + it('should support template directives', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Content: {$startTagDiv}before{$startTagSpan}middle{$closeTagSpan}after{$closeTagDiv}!': + 'Contenu: {$startTagDiv}avant{$startTagSpan}milieu{$closeTagSpan}après{$closeTagDiv}!' + } + }); + const fixture = initWithTemplate( + AppComp, + `
Content:
beforemiddleafter
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Contenu:
avantmilieuaprès
!
`); + + fixture.componentRef.instance.visible = false; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
Contenu: !
`); + }); + + it('should support multiple i18n blocks', () => { + ɵi18nConfigureLocalize({ + translations: { + 'trad {$interpolation}': 'traduction {$interpolation}', + 'start {$interpolation} middle {$interpolation_1} end': + 'start {$interpolation_1} middle {$interpolation} end', + '{$startTagC}trad{$closeTagC}{$startTagD}{$closeTagD}{$startTagE}{$closeTagE}': + '{$startTagE}{$closeTagE}{$startTagC}traduction{$closeTagC}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ trad {{name}} + hello + + trad + + + +
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
traduction Angular hello traduction
`); + }); + + it('should support multiple sibling i18n blocks', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Section 1': 'Section un', + 'Section 2': 'Section deux', + 'Section 3': 'Section trois', + } + }); + const fixture = initWithTemplate(AppComp, ` +
+
Section 1
+
Section 2
+
Section 3
+
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Section un
Section deux
Section trois
`); + }); + + it('should support multiple sibling i18n blocks inside of a template directive', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Section 1': 'Section un', + 'Section 2': 'Section deux', + 'Section 3': 'Section trois', + } + }); + const fixture = initWithTemplate(AppComp, ` +
    +
  • Section 1
  • +
  • Section 2
  • +
  • Section 3
  • +
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
  • Section un
  • Section deux
  • Section trois
  • Section un
  • Section deux
  • Section trois
  • Section un
  • Section deux
  • Section trois
`); + }); + + it('should properly escape quotes in content', () => { + ɵi18nConfigureLocalize({ + translations: { + '\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"' + } + }); + const fixture = + initWithTemplate(AppComp, `
'Single quotes' and "Double quotes"
`); + + expect(fixture.nativeElement.innerHTML) + .toEqual('
\'Guillemets simples\' et "Guillemets doubles"
'); + }); + + it('should correctly bind to context in nested template', () => { + ɵi18nConfigureLocalize({translations: {'Item {$interpolation}': 'Article {$interpolation}'}}); + const fixture = initWithTemplate(AppComp, ` +
+
Item {{ id }}
+
+ `); + + const element = fixture.nativeElement; + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i]; + expect(child).toHaveText(`Article ${i + 1}`); + } + }); + + it('should ignore i18n attributes on self-closing tags', () => { + const fixture = initWithTemplate(AppComp, ''); + expect(fixture.nativeElement.innerHTML).toBe(``); + }); + + it('should handle i18n attribute with directives', () => { + ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const fixture = initWithTemplate(AppComp, `
Hello {{ name }}
`); + expect(fixture.nativeElement.firstChild).toHaveText('Bonjour Angular'); + }); + + it('should work correctly with event listeners', () => { + ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + + @Component( + {selector: 'app-comp', template: `
Hello {{ name }}
`}) + class ListenerComp { + name = `Angular`; + clicks = 0; + + onClick() { this.clicks++; } + } + + TestBed.configureTestingModule({declarations: [ListenerComp]}); + const fixture = TestBed.createComponent(ListenerComp); + fixture.detectChanges(); + + const element = fixture.nativeElement.firstChild; + const instance = fixture.componentInstance; + + expect(element).toHaveText('Bonjour Angular'); + expect(instance.clicks).toBe(0); + + element.click(); + expect(instance.clicks).toBe(1); + }); + + describe('ng-container and ng-template support', () => { + it('should support ng-container', () => { + ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); + const fixture = initWithTemplate(AppComp, `text`); + expect(fixture.nativeElement.innerHTML).toEqual(`texte`); + }); + + it('should handle single translation message within ng-template', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const fixture = + initWithTemplate(AppComp, `Hello {{ name }}`); + + const element = fixture.nativeElement; + expect(element).toHaveText('Bonjour Angular'); + }); + + it('should be able to act as child elements inside i18n block (plain text content)', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}': + '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + Hello + + + Bye + +
+ `); + + const element = fixture.nativeElement.firstChild; + expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Bonjour Au revoir'); + }); + + it('should be able to act as child elements inside i18n block (text + tags)', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgContainer}': + '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgContainer}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + Hello + + + Hello + +
+ `); + + const element = fixture.nativeElement; + const spans = element.getElementsByTagName('span'); + for (let i = 0; i < spans.length; i++) { + expect(spans[i]).toHaveText('Bonjour'); + } + }); + + it('should be able to handle deep nested levels with templates', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagSpan} Hello - 1 {$closeTagSpan}{$startTagSpan_1} Hello - 2 {$startTagSpan_1} Hello - 3 {$startTagSpan_1} Hello - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Hello - 5 {$closeTagSpan}': + '{$startTagSpan} Bonjour - 1 {$closeTagSpan}{$startTagSpan_1} Bonjour - 2 {$startTagSpan_1} Bonjour - 3 {$startTagSpan_1} Bonjour - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Bonjour - 5 {$closeTagSpan}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + Hello - 1 + + + Hello - 2 + + Hello - 3 + + Hello - 4 + + + + + Hello - 5 + +
+ `); + + const element = fixture.nativeElement; + const spans = element.getElementsByTagName('span'); + for (let i = 0; i < spans.length; i++) { + expect(spans[i].innerHTML).toContain(`Bonjour - ${i + 1}`); + } + }); + + it('should handle self-closing tags as content', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}': + '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}' + } + }); + const content = `My logo`; + const fixture = initWithTemplate(AppComp, ` + + ${content} + + + ${content} + + `); + + const element = fixture.nativeElement; + const spans = element.getElementsByTagName('span'); + for (let i = 0; i < spans.length; i++) { + const child = spans[i]; + expect(child).toHaveText('Mon logo'); + } + }); + }); + + describe('should support ICU expressions', () => { + it('with no root node', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': + '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}' + } + }); + const fixture = + initWithTemplate(AppComp, `{count, select, 10 {ten} 20 {twenty} other {other}}`); + + const element = fixture.nativeElement; + expect(element).toHaveText('autre'); + }); + + it('with no i18n tag', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': + '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}' + } + }); + const fixture = initWithTemplate( + AppComp, `
{count, select, 10 {ten} 20 {twenty} other {other}}
`); + + const element = fixture.nativeElement; + expect(element).toHaveText('autre'); + }); + + it('multiple', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}', + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } - {name, select, + other {({{name}})} + }
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
aucun email! - (Angular)
`); + + fixture.componentRef.instance.count = 4; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
4 emails - (Angular)
`); + + fixture.componentRef.instance.count = 0; + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
aucun email! - (John)
`); + }); + + it('with custom interpolation config', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} other {{$interpolation}}}': + '{VAR_SELECT, select, 10 {dix} other {{$interpolation}}}' + } + }); + const interpolation = ['{%', '%}'] as[string, string]; + TestBed.overrideComponent(AppComp, {set: {interpolation}}); + const fixture = + initWithTemplate(AppComp, `
{count, select, 10 {ten} other {{% name %}}}
`); + + expect(fixture.nativeElement).toHaveText(`Angular`); + }); + + it('inside HTML elements', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}', + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } - {name, select, + other {({{name}})} + }
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
aucun email! - (Angular)
`); + + fixture.componentRef.instance.count = 4; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
4 emails - (Angular)
`); + + fixture.componentRef.instance.count = 0; + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
aucun email! - (John)
`); + }); + + it('inside template directives', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{name, select, + other {({{name}})} + }
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
(Angular)
`); + + fixture.componentRef.instance.visible = false; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('inside ng-container', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `{name, select, + other {({{name}})} + }`); + expect(fixture.nativeElement.innerHTML).toEqual(`(Angular)`); + }); + + it('inside ', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': + '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}' + } + }); + const fixture = initWithTemplate(AppComp, ` + + {count, select, 10 {ten} 20 {twenty} other {other}} + + `); + + const element = fixture.nativeElement; + expect(element).toHaveText('autre'); + }); + + it('nested', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_PLURAL, plural, =0 {zero} other {{$interpolation} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}': + '{VAR_PLURAL, plural, =0 {zero} other {{$interpolation} {VAR_SELECT, select, cat {chats} dog {chients} other {animaux}}!}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{count, plural, + =0 {zero} + other {{{count}} {name, select, + cat {cats} + dog {dogs} + other {animals} + }!} + }
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
zero
`); + + fixture.componentRef.instance.count = 4; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
4 animaux!
`); + }); + }); + + describe('should support attributes', () => { + it('text', () => { + ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); + const fixture = initWithTemplate(AppComp, `
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('interpolations', () => { + ɵi18nConfigureLocalize( + {translations: {'hello {$interpolation}': 'bonjour {$interpolation}'}}); + const fixture = + initWithTemplate(AppComp, `
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('multiple attributes', () => { + ɵi18nConfigureLocalize( + {translations: {'hello {$interpolation}': 'bonjour {$interpolation}'}}); + const fixture = initWithTemplate( + AppComp, + `
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
`); + + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
`); + }); + + it('on removed elements', () => { + ɵi18nConfigureLocalize( + {translations: {'text': 'texte', '{$startTagSpan}content{$closeTagSpan}': 'contenu'}}); + const fixture = + initWithTemplate(AppComp, `
content
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
contenu
`); + }); + + it('with custom interpolation config', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const interpolation = ['{%', '%}'] as[string, string]; + TestBed.overrideComponent(AppComp, {set: {interpolation}}); + const fixture = + initWithTemplate(AppComp, `
`); + + const element = fixture.nativeElement.firstChild; + expect(element.title).toBe('Bonjour Angular'); + }); + + it('in nested template', () => { + ɵi18nConfigureLocalize({translations: {'Item {$interpolation}': 'Article {$interpolation}'}}); + const fixture = initWithTemplate(AppComp, ` +
+
+
`); + + const element = fixture.nativeElement; + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i]; + expect((child as any).innerHTML).toBe(`
`); + } + }); + + it('should add i18n attributes on self-closing tags', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const fixture = + initWithTemplate(AppComp, ``); + + const element = fixture.nativeElement.firstChild; + expect(element.title).toBe('Bonjour Angular'); + }); + }); + + it('should work with directives and host bindings', () => { + let directiveInstances: ClsDir[] = []; + + @Directive({selector: '[test]'}) + class ClsDir { + @HostBinding('className') + klass = 'foo'; + + constructor() { directiveInstances.push(this); } + } + + @Component({ + selector: `my-app`, + template: ` +
+ trad: {exp1, plural, + =0 {no emails!} + =1 {one email} + other {{{exp1}} emails} + } +
` + }) + class MyApp { + exp1 = 1; + exp2 = 2; + } + + TestBed.configureTestingModule({declarations: [ClsDir, MyApp]}); + ɵi18nConfigureLocalize({ + translations: { + 'start {$interpolation} middle {$interpolation_1} end': + 'début {$interpolation_1} milieu {$interpolation} fin', + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}', + ' trad: {$icu}': ' traduction: {$icu}' + } + }); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
traduction: un email
`); + + directiveInstances.forEach(instance => instance.klass = 'bar'); + fixture.componentRef.instance.exp1 = 2; + fixture.componentRef.instance.exp2 = 3; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
traduction: 2 emails
`); + }); + + it('should support adding/moving/removing nodes', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagDiv2}{$closeTagDiv2}{$startTagDiv3}{$closeTagDiv3}{$startTagDiv4}{$closeTagDiv4}{$startTagDiv5}{$closeTagDiv5}{$startTagDiv6}{$closeTagDiv6}{$startTagDiv7}{$closeTagDiv7}{$startTagDiv8}{$closeTagDiv8}': + '{$startTagDiv2}{$closeTagDiv2}{$startTagDiv8}{$closeTagDiv8}{$startTagDiv4}{$closeTagDiv4}{$startTagDiv5}{$closeTagDiv5}Bonjour monde{$startTagDiv3}{$closeTagDiv3}{$startTagDiv7}{$closeTagDiv7}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + + + + + + +
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
Bonjour monde
`); + }); + + describe('projection', () => { + it('should project the translations', () => { + @Component({selector: 'child', template: '

'}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` +
+ I am projected from + {{name}} + + + +
` + }) + class Parent { + name: string = 'Parent'; + } + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({ + translations: { + 'Child of {$interpolation}': 'Enfant de {$interpolation}', + '{$startTagChild}I am projected from {$startBoldText}{$interpolation}{$startTagRemoveMe_1}{$closeTagRemoveMe_1}{$closeBoldText}{$startTagRemoveMe_2}{$closeTagRemoveMe_2}{$closeTagChild}{$startTagRemoveMe_3}{$closeTagRemoveMe_3}': + '{$startTagChild}Je suis projeté depuis {$startBoldText}{$interpolation}{$closeBoldText}{$closeTagChild}' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `

Je suis projeté depuis Parent

`); + }); + + it('should project a translated i18n block', () => { + @Component({selector: 'child', template: '

'}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` +
+ + + I am projected from {{name}} + + +
` + }) + class Parent { + name: string = 'Parent'; + } + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({ + translations: { + 'Child of {$interpolation}': 'Enfant de {$interpolation}', + 'I am projected from {$interpolation}': 'Je suis projeté depuis {$interpolation}' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `

Je suis projeté depuis Parent

`); + + // it should be able to render a new component with the same template code + const fixture2 = TestBed.createComponent(Parent); + fixture2.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(fixture2.nativeElement.innerHTML); + + fixture2.componentRef.instance.name = 'Parent 2'; + fixture2.detectChanges(); + expect(fixture2.nativeElement.innerHTML) + .toEqual( + `

Je suis projeté depuis Parent 2

`); + + // The first fixture should not have changed + expect(fixture.nativeElement.innerHTML).not.toEqual(fixture2.nativeElement.innerHTML); + }); + + it('should re-project translations when multiple projections', () => { + @Component({selector: 'grand-child', template: '
'}) + class GrandChild { + } + + @Component( + {selector: 'child', template: ''}) + class Child { + } + + @Component({selector: 'parent', template: `Hello World!`}) + class Parent { + name: string = 'Parent'; + } + + TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]}); + ɵi18nConfigureLocalize({ + translations: { + '{$startBoldText}Hello{$closeBoldText} World!': + '{$startBoldText}Bonjour{$closeBoldText} monde!' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('
Bonjour monde!
'); + }); + + // FW-1319 Runtime i18n should be able to remove projected placeholders + xit('should be able to remove projected placeholders', () => { + @Component({selector: 'grand-child', template: '
'}) + class GrandChild { + } + + @Component( + {selector: 'child', template: ''}) + class Child { + } + + @Component({selector: 'parent', template: `Hello World!`}) + class Parent { + name: string = 'Parent'; + } + + TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]}); + ɵi18nConfigureLocalize( + {translations: {'{$startBoldText}Hello{$closeBoldText} World!': 'Bonjour monde!'}}); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('
Bonjour monde!
'); + }); + + // FW-1312: Wrong i18n code generated by the compiler when the template has 2 empty `span` + xit('should project translations with selectors', () => { + @Component({selector: 'child', template: ``}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` + + + + + ` + }) + class Parent { + } + + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({ + translations: { + '{$startTagSpan}{$closeTagSpan}{$startTagSpan_1}{$closeTagSpan}': + '{$startTagSpan}Contenu{$closeTagSpan}' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('Contenu'); + }); + }); + + describe('queries', () => { + function toHtml(element: Element): string { + return element.innerHTML.replace(/\sng-reflect-\S*="[^"]*"/g, '') + .replace(//g, ''); + } + + it('detached nodes should still be part of query', () => { + @Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'}) + class TextDirective { + // TODO(issue/24571): remove '!'. + text !: string; + constructor() {} + } + + @Component({selector: 'div-query', template: ''}) + class DivQuery { + // TODO(issue/24571): remove '!'. + @ContentChild(TemplateRef) template !: TemplateRef; + + // TODO(issue/24571): remove '!'. + @ViewChild('vc', {read: ViewContainerRef}) + vc !: ViewContainerRef; + + // TODO(issue/24571): remove '!'. + @ContentChildren(TextDirective, {descendants: true}) + query !: QueryList; + + create() { this.vc.createEmbeddedView(this.template); } + + destroy() { this.vc.clear(); } + } + + TestBed.configureTestingModule({declarations: [TextDirective, DivQuery]}); + ɵi18nConfigureLocalize({ + translations: { + '{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}': + '{$startTagNgTemplate}Contenu{$closeTagNgTemplate}' + } + }); + const fixture = initWithTemplate(AppComp, ` + + +
+
+ Content +
+
+
+
+ `); + const q = fixture.debugElement.children[0].references.q; + expect(q.query.length).toEqual(0); + + // Create embedded view + q.create(); + fixture.detectChanges(); + expect(q.query.length).toEqual(1); + expect(toHtml(fixture.nativeElement)) + .toEqual(`Contenu`); + + // Disable ng-if + fixture.componentInstance.visible = false; + fixture.detectChanges(); + expect(q.query.length).toEqual(0); + expect(toHtml(fixture.nativeElement)) + .toEqual(`Contenu`); + }); + }); +}); + +function initWithTemplate(compType: Type, template: string) { + TestBed.overrideComponent(compType, {set: {template}}); + const fixture = TestBed.createComponent(compType); + fixture.detectChanges(); + return fixture; +} + +@Component({selector: 'app-comp', template: ``}) +class AppComp { + name = `Angular`; + visible = true; + count = 0; +} + +@Directive({ + selector: '[tplRef]', +}) +class DirectiveWithTplRef { + constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {} + ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); } +} diff --git a/packages/core/test/i18n_integration_spec.ts b/packages/core/test/i18n_integration_spec.ts deleted file mode 100644 index af09a356a7..0000000000 --- a/packages/core/test/i18n_integration_spec.ts +++ /dev/null @@ -1,640 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; -import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {onlyInIvy} from '@angular/private/testing'; - -@Directive({ - selector: '[tplRef]', -}) -class DirectiveWithTplRef { - constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {} - ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); } -} - -@Component({selector: 'my-comp', template: ''}) -class MyComp { - name = 'John'; - items = ['1', '2', '3']; - obj = {a: {b: 'value'}}; - visible = true; - age = 20; - count = 2; - otherLabel = 'other label'; - clicks = 0; - - onClick() { this.clicks++; } -} - -const TRANSLATIONS: any = { - 'one': 'un', - 'two': 'deux', - 'more than two': 'plus que deux', - 'ten': 'dix', - 'twenty': 'vingt', - 'other': 'autres', - 'Hello': 'Bonjour', - 'Hello {$interpolation}': 'Bonjour {$interpolation}', - 'Bye': 'Au revoir', - 'Item {$interpolation}': 'Article {$interpolation}', - '\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"', - 'My logo': 'Mon logo', - '{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)', - '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}': - '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}', - '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}': - '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}', - '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgContainer}': - '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgContainer}', - '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Hello{$closeTagSpan}{$closeTagNgContainer}': - '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Bonjour{$closeTagSpan}{$closeTagNgContainer}', - '{$startTagSpan} Hello - 1 {$closeTagSpan}{$startTagSpan_1} Hello - 2 {$startTagSpan_1} Hello - 3 {$startTagSpan_1} Hello - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Hello - 5 {$closeTagSpan}': - '{$startTagSpan} Bonjour - 1 {$closeTagSpan}{$startTagSpan_1} Bonjour - 2 {$startTagSpan_1} Bonjour - 3 {$startTagSpan_1} Bonjour - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Bonjour - 5 {$closeTagSpan}', - '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': - '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autres}}', - '{VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}': - '{VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}', - '{VAR_SELECT, select, 10 {10 - {$startBoldText}ten{$closeBoldText}} 20 {20 - {$startItalicText}twenty{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}other{$closeUnderlinedText}{$closeTagDiv}}}': - '{VAR_SELECT, select, 10 {10 - {$startBoldText}dix{$closeBoldText}} 20 {20 - {$startItalicText}vingt{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}autres{$closeUnderlinedText}{$closeTagDiv}}}', - '{VAR_SELECT_2, select, 10 {ten - {VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}} 20 {twenty - {VAR_SELECT_1, select, 1 {one} 2 {two} other {more than two}}} other {other}}': - '{VAR_SELECT_2, select, 10 {dix - {VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}} 20 {vingt - {VAR_SELECT_1, select, 1 {un} 2 {deux} other {plus que deux}}} other {autres}}', - '{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}': - '{$startTagNgTemplate}Contenu{$closeTagNgTemplate}' -}; - -const getFixtureWithOverrides = (overrides = {}) => { - TestBed.overrideComponent(MyComp, {set: overrides}); - const fixture = TestBed.createComponent(MyComp); - fixture.detectChanges(); - return fixture; -}; - -onlyInIvy('Ivy i18n logic').describe('i18n', function() { - - beforeEach(() => { - ɵi18nConfigureLocalize({translations: TRANSLATIONS}); - TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTplRef]}); - }); - - describe('attributes', () => { - it('should translate static attributes', () => { - const title = 'Hello'; - const template = `
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour'); - }); - - it('should support interpolation', () => { - const title = 'Hello {{ name }}'; - const template = `
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - - it('should support interpolation with custom interpolation config', () => { - const title = 'Hello {% name %}'; - const template = `
`; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - - it('should correctly bind to context in nested template', () => { - const title = 'Item {{ id }}'; - const template = ` -
-
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect((child as any).innerHTML).toBe(`
`); - } - }); - - it('should work correctly when placed on i18n root node', () => { - const title = 'Hello {{ name }}'; - const content = 'Hello'; - const template = ` -
${content}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - expect(element).toHaveText('Bonjour'); - }); - - it('should add i18n attributes on self-closing tags', () => { - const title = 'Hello {{ name }}'; - const template = ``; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - }); - - describe('nested nodes', () => { - it('should handle static content', () => { - const content = 'Hello'; - const template = `
${content}
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour'); - }); - - it('should support interpolation', () => { - const content = 'Hello {{ name }}'; - const template = `
${content}
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should support interpolation with custom interpolation config', () => { - const content = 'Hello {% name %}'; - const template = `
${content}
`; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should support interpolations with complex expressions', () => { - const template = `
{{ name | uppercase }} - {{ obj?.a?.b }}
`; - const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('JOHN - value (fr)'); - }); - - it('should properly escape quotes in content', () => { - const content = `'Single quotes' and "Double quotes"`; - const template = `
${content}
`; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('\'Guillemets simples\' et "Guillemets doubles"'); - }); - - it('should correctly bind to context in nested template', () => { - const content = 'Item {{ id }}'; - const template = ` -
-
${content}
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect(child).toHaveText(`Article ${i + 1}`); - } - }); - - it('should handle i18n attributes inside i18n section', () => { - const title = 'Hello {{ name }}'; - const template = ` -
-
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const content = `
`; - expect(element.innerHTML).toBe(content); - }); - - it('should handle i18n blocks in nested templates', () => { - const content = 'Hello {{ name }}'; - const template = ` -
-
${content}
-
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.children[0]).toHaveText('Bonjour John'); - }); - - it('should ignore i18n attributes on self-closing tags', () => { - const template = ''; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element.innerHTML).toBe(template.replace(' i18n', '')); - }); - - it('should handle i18n attribute with directives', () => { - const content = 'Hello {{ name }}'; - const template = ` -
${content}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should work correctly with event listeners', () => { - const content = 'Hello {{ name }}'; - const template = ` -
${content}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const instance = fixture.componentInstance; - - expect(element).toHaveText('Bonjour John'); - expect(instance.clicks).toBe(0); - - element.click(); - expect(instance.clicks).toBe(1); - }); - }); - - describe('ng-container and ng-template support', () => { - it('should handle single translation message within ng-container', () => { - const content = 'Hello {{ name }}'; - const template = ` - ${content} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should handle single translation message within ng-template', () => { - const content = 'Hello {{ name }}'; - const template = ` - ${content} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('Bonjour John'); - }); - - it('should be able to act as child elements inside i18n block (plain text content)', () => { - const hello = 'Hello'; - const bye = 'Bye'; - const template = ` -
- - ${hello} - - - ${bye} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Bonjour Au revoir'); - }); - - it('should be able to act as child elements inside i18n block (text + tags)', () => { - const content = 'Hello'; - const template = ` -
- - ${content} - - - ${content} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - expect(spans[i]).toHaveText('Bonjour'); - } - }); - - it('should be able to handle deep nested levels with templates', () => { - const content = 'Hello'; - const template = ` -
- - ${content} - 1 - - - ${content} - 2 - - ${content} - 3 - - ${content} - 4 - - - - - ${content} - 5 - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - expect(spans[i].innerHTML).toContain(`Bonjour - ${i + 1}`); - } - }); - - it('should handle self-closing tags as content', () => { - const label = 'My logo'; - const content = `${label}`; - const template = ` - - ${content} - - - ${content} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - const child = spans[i]; - expect(child).toHaveText('Mon logo'); - } - }); - }); - - describe('ICU logic', () => { - it('should handle single ICUs', () => { - const template = ` -
{age, select, 10 {ten} 20 {twenty} other {other}}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support ICU-only templates', () => { - const template = ` - {age, select, 10 {ten} 20 {twenty} other {other}} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support ICUs generated outside of i18n blocks', () => { - const template = ` -
{age, select, 10 {ten} 20 {twenty} other {other}}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support interpolation', () => { - const template = ` -
{age, select, 10 {ten} other {{{ otherLabel }}}}
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText(fixture.componentInstance.otherLabel); - }); - - it('should support interpolation with custom interpolation config', () => { - const template = ` -
{age, select, 10 {ten} other {{% otherLabel %}}}
- `; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement; - expect(element).toHaveText(fixture.componentInstance.otherLabel); - }); - - it('should handle ICUs with HTML tags inside', () => { - const template = ` -
- {age, select, 10 {10 - ten} 20 {20 - twenty} other {
other
}} -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const italicTags = element.getElementsByTagName('i'); - expect(italicTags.length).toBe(1); - expect(italicTags[0].innerHTML).toBe('vingt'); - }); - - it('should handle multiple ICUs in one block', () => { - const template = ` -
- {age, select, 10 {ten} 20 {twenty} other {other}} - - {count, select, 1 {one} 2 {two} other {more than two}} -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); - - it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => { - const template = ` -
- - {age, select, 10 {ten} 20 {twenty} other {other}} - - - {count, select, 1 {one} 2 {two} other {more than two}} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(2); - expect(spans[0]).toHaveText('vingt'); - expect(spans[1]).toHaveText('deux'); - }); - - it('should handle ICUs inside a template in i18n block', () => { - const template = ` -
- - {age, select, 10 {ten} 20 {twenty} other {other}} - -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(1); - expect(spans[0]).toHaveText('vingt'); - }); - - it('should handle nested icus', () => { - const template = ` -
- {age, select, - 10 {ten - {count, select, 1 {one} 2 {two} other {more than two}}} - 20 {twenty - {count, select, 1 {one} 2 {two} other {more than two}}} - other {other}} -
- `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); - - it('should handle ICUs inside ', () => { - const template = ` - - {age, select, 10 {ten} 20 {twenty} other {other}} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should handle ICUs inside ', () => { - const template = ` - - {age, select, 10 {ten} 20 {twenty} other {other}} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - }); - - describe('queries', () => { - function toHtml(element: Element): string { - return element.innerHTML.replace(/\sng-reflect-\S*="[^"]*"/g, '') - .replace(//g, ''); - } - - it('detached nodes should still be part of query', () => { - const template = ` - - -
-
- Content -
-
-
-
- `; - - @Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'}) - class TextDirective { - // TODO(issue/24571): remove '!'. - text !: string; - constructor() {} - } - - @Component({selector: 'div-query', template: ''}) - class DivQuery { - // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) template !: TemplateRef; - - // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef}) - vc !: ViewContainerRef; - - // TODO(issue/24571): remove '!'. - @ContentChildren(TextDirective, {descendants: true}) - query !: QueryList; - - create() { this.vc.createEmbeddedView(this.template); } - - destroy() { this.vc.clear(); } - } - - TestBed.configureTestingModule({declarations: [TextDirective, DivQuery]}); - const fixture = getFixtureWithOverrides({template}); - const q = fixture.debugElement.children[0].references.q; - expect(q.query.length).toEqual(0); - - // Create embedded view - q.create(); - fixture.detectChanges(); - expect(q.query.length).toEqual(1); - expect(toHtml(fixture.nativeElement)) - .toEqual(`Contenu`); - - // Disable ng-if - fixture.componentInstance.visible = false; - fixture.detectChanges(); - expect(q.query.length).toEqual(0); - expect(toHtml(fixture.nativeElement)) - .toEqual(`Contenu`); - }); - }); - - it('should handle multiple i18n sections', () => { - const template = ` -
Section 1
-
Section 2
-
Section 3
- `; - const fixture = getFixtureWithOverrides({template}); - expect(fixture.nativeElement.innerHTML) - .toBe('
Section 1
Section 2
Section 3
'); - }); - - it('should handle multiple i18n sections inside of *ngFor', () => { - const template = ` -
    -
  • Section 1
  • -
  • Section 2
  • -
  • Section 3
  • -
- `; - const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect(child.innerHTML).toBe(`
  • Section 1
  • Section 2
  • Section 3
  • `); - } - }); -}); diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 8f47b2509a..10df144f31 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -6,26 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgForOfContext} from '@angular/common'; - import {noop} from '../../../compiler/src/render3/view/util'; -import {Component as _Component} from '../../src/core'; -import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/definition'; -import {getTranslationForTemplate, ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; -import {ɵɵallocHostVars, ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵnextContext, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n} from '../../src/render3/interfaces/i18n'; -import {AttributeMarker} from '../../src/render3/interfaces/node'; +import {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; +import {ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view'; -import {getNativeByIndex, getTNode} from '../../src/render3/util/view_utils'; - -import {NgForOf, NgIf} from './common_with_def'; -import {ComponentFixture, TemplateFixture} from './render_util'; - -const Component: typeof _Component = function(...args: any[]): any { - // In test we use @Component for documentation only so it's safe to mock out the implementation. - return () => undefined; -} as any; +import {getNativeByIndex} from '../../src/render3/util/view_utils'; +import {TemplateFixture} from './render_util'; describe('Runtime i18n', () => { describe('getTranslationForTemplate', () => { @@ -83,7 +70,6 @@ describe('Runtime i18n', () => { const index = 0; const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index); - // Check debug const debugOps = (opCodes as any).create.debug !.operations; expect(debugOps[0].__raw_opCode).toBe('simple text'); @@ -586,319 +572,6 @@ describe('Runtime i18n', () => { }); }); - describe(`i18nEnd`, () => { - it('for text', () => { - const MSG_DIV = `simple text`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - expect(fixture.html).toEqual(`
    ${MSG_DIV}
    `); - }); - - it('for bindings', () => { - const MSG_DIV = `Hello �0�!`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - - // But it should have created an empty text node in `viewData` - const textTNode = fixture.hostView[HEADER_OFFSET + 2] as Node; - expect(textTNode.nodeType).toEqual(Node.TEXT_NODE); - }); - - it('for elements', () => { - const MSG_DIV = `Hello �#3�world�/#3� and �#2�universe�/#2�!`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'div'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, null, 4); - - expect(fixture.html).toEqual('
    Hello world and
    universe
    !
    '); - }); - - it('for translations without top level element', () => { - // When it's the first node - let MSG_DIV = `Hello world`; - let fixture = prepareFixture(() => { ɵɵi18n(0, MSG_DIV); }, null, 1); - - expect(fixture.html).toEqual('Hello world'); - - // When the first node is a text node - MSG_DIV = ` world`; - fixture = prepareFixture(() => { - ɵɵtext(0, 'Hello'); - ɵɵi18n(1, MSG_DIV); - }, null, 2); - - expect(fixture.html).toEqual('Hello world'); - - // When the first node is an element - fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵtext(1, 'Hello'); - ɵɵelementEnd(); - ɵɵi18n(2, MSG_DIV); - }, null, 3); - - expect(fixture.html).toEqual('
    Hello
    world'); - - // When there is a node after - MSG_DIV = `Hello `; - fixture = prepareFixture(() => { - ɵɵi18n(0, MSG_DIV); - ɵɵtext(1, 'world'); - }, null, 2); - - expect(fixture.html).toEqual('Hello world'); - }); - - it('for deleted placeholders', () => { - const MSG_DIV = `Hello �#3�world�/#3�`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV); - { - ɵɵelement(2, 'div'); // Will be removed - ɵɵelement(3, 'span'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - ɵɵelementStart(4, 'div'); - { ɵɵtext(5, '!'); } - ɵɵelementEnd(); - }, null, 6); - - expect(fixture.html).toEqual('
    Hello world
    !
    '); - }); - - it('for sub-templates', () => { - // Template: `
    Content:
    beforemiddleafter
    !
    `; - const MSG_DIV = - `Content: �*2:1��#1:1�before�*2:2��#1:2�middle�/#1:2��/*2:2�after�/#1:1��/*2:1�!`; - - function subTemplate_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 1); - ɵɵelementStart(1, 'div'); - ɵɵtemplate(2, subTemplate_2, 2, 0, 'span', [AttributeMarker.Template, 'ngIf']); - ɵɵelementEnd(); - ɵɵi18nEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', ɵɵbind(true)); - } - } - - function subTemplate_2(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 2); - ɵɵelement(1, 'span'); - ɵɵi18nEnd(); - } - } - - class MyApp { - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - directives: [NgIf], - factory: () => new MyApp(), - consts: 3, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵtemplate(2, subTemplate_1, 3, 1, 'div', [AttributeMarker.Template, 'ngIf']); - ɵɵi18nEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
    Content:
    beforemiddleafter
    !
    '); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - } - {�0�, select, - other {(�0�)} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    -
    '); - }); - - it('for multiple ICU expressions inside html', () => { - const MSG_DIV = `�#2�{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }�/#2��#3�{�0�, select, - other {(�0�)} - }�/#3�`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'span'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, null, 4); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - }); - - it('for ICU expressions inside templates', () => { - const MSG_DIV = `�*2:1��#1:1�{�0:1�, plural, - =0 {no emails!} - =1 {one email} - other {�0:1� emails} - }�/#1:1��/*2:1�`; - - function subTemplate_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 1); - ɵɵelement(1, 'span'); - ɵɵi18nEnd(); - } - if (rf & RenderFlags.Update) { - const ctx = ɵɵnextContext(); - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(0); - } - } - - class MyApp { - value0 = 0; - value1 = 'emails label'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - directives: [NgIf], - factory: () => new MyApp(), - consts: 3, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵtemplate(2, subTemplate_1, 2, 2, 'span', [AttributeMarker.Template, 'ngIf']); - ɵɵi18nEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
    no emails!
    '); - - // Update the value - fixture.component.value0 = 3; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    3 emails
    '); - }); - - it('for ICU expressions inside ', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - { - ɵɵelementContainerStart(1); - { ɵɵi18n(2, MSG_DIV); } - ɵɵelementContainerEnd(); - } - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(0)); - ɵɵi18nExp(ɵɵbind('more than one')); - ɵɵi18nApply(2); - }, - 3, 2); - - expect(fixture.html).toEqual('
    no emails!
    '); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    '); - }); - }); - describe(`i18nAttribute`, () => { it('for text', () => { const MSG_title = `Hello world!`; @@ -975,1140 +648,6 @@ describe('Runtime i18n', () => { }); }); - describe(`i18nExp & i18nApply`, () => { - it('for text bindings', () => { - const MSG_DIV = `Hello �0�!`; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
    Hello world!
    '); - }); - - it('for attribute bindings', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html).toEqual('
    '); - }); - - it('for attributes with no bindings', () => { - const MSG_title = `Hello world!`; - const MSG_div_attr = ['title', MSG_title]; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { ɵɵi18nApply(1); }, 2, 1); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - }); - - it('for multiple attribute bindings', () => { - const MSG_title = `Hello �0� and �1�, again �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value0: 'world', value1: 'universe'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value0 = 'earth'; - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value0 = 'earthlings'; - ctx.value1 = 'martians'; - fixture.update(); - expect(fixture.html) - .toEqual('
    '); - }); - - it('for bindings of multiple attributes', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    '); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html) - .toEqual('
    '); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - expect(fixture.html).toEqual('
    no emails!
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    no emails!
    '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html).toEqual('
    one email
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual('
    10 emails
    '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual('
    10 emails
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html).toEqual('
    no emails!
    '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - } - {�0�, select, - other {(�0�)} - }`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - expect(fixture.html) - .toEqual('
    no emails! - (0)
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual('
    no emails! - (0)
    '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html).toEqual('
    one email - (1)
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails - (10)
    '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails - (10)
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html) - .toEqual('
    no emails! - (0)
    '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `�#2�{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }�/#2��#3�{�0�, select, - other {(�0�)} - }�/#3�`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'span'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 4, 2); - expect(fixture.html) - .toEqual( - '
    no emails!(0)
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
    no emails!(0)
    '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    one email(1)
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails(10)
    '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    10 emails(10)
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html) - .toEqual( - '
    no emails!(0)
    '); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const ctx = {value0: 0, value1: 'cat'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - - expect(fixture.html).toEqual('
    zero
    '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    zero
    '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html).toEqual('
    10 cats!
    '); - - ctx.value1 = 'squirrel'; - fixture.update(); - expect(fixture.html).toEqual('
    10 animals!
    '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html).toEqual('
    zero
    '); - }); - }); - - describe('integration', () => { - it('should support multiple i18n blocks', () => { - // Translated template: - //
    - // - // trad {{exp1}} - // - // hello - // - // - // trad - // - //
    - - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_2_ATTR = ['title', `start �1� middle �0� end`]; - const MSG_DIV_2 = `�#9��/#9��#7�trad�/#7�`; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 10, - vars: 2, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'a'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵtext(3, 'hello'); - ɵɵelementStart(4, 'b'); - { - ɵɵi18nAttributes(5, MSG_DIV_2_ATTR); - ɵɵi18nStart(6, MSG_DIV_2); - { - ɵɵelement(7, 'c'); - ɵɵelement(8, 'd'); // will be removed - ɵɵelement(9, 'e'); // will be moved before `c` - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - `
    trad 1hellotrad
    `); - }); - - it('should support multiple sibling i18n blocks', () => { - // Translated template: - //
    - //
    Section 1
    - //
    Section 2
    - //
    Section 3
    - //
    - - const MSG_DIV_1 = `Section 1`; - const MSG_DIV_2 = `Section 2`; - const MSG_DIV_3 = `Section 3`; - - class MyApp { - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 7, - vars: 0, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'div'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵelementStart(3, 'div'); - { ɵɵi18n(4, MSG_DIV_2); } - ɵɵelementEnd(); - ɵɵelementStart(5, 'div'); - { ɵɵi18n(6, MSG_DIV_3); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nApply(2); - ɵɵi18nApply(4); - ɵɵi18nApply(6); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual(`
    Section 1
    Section 2
    Section 3
    `); - }); - - it('should support multiple sibling i18n blocks inside of *ngFor', () => { - // Translated template: - //
      - //
    • Section 1
    • - //
    • Section 2
    • - //
    • Section 3
    • - //
    - - const MSG_DIV_1 = `Section 1`; - const MSG_DIV_2 = `Section 2`; - const MSG_DIV_3 = `Section 3`; - - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - ɵɵelementStart(1, 'li'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵelementStart(3, 'li'); - { ɵɵi18n(4, MSG_DIV_2); } - ɵɵelementEnd(); - ɵɵelementStart(5, 'li'); - { ɵɵi18n(6, MSG_DIV_3); } - ɵɵelementEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nApply(2); - ɵɵi18nApply(4); - ɵɵi18nApply(6); - } - } - - class MyApp { - items: string[] = ['1', '2', '3']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, liTemplate, 7, 0, 'ul', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - `
    • Section 1
    • Section 2
    • Section 3
    • Section 1
    • Section 2
    • Section 3
    • Section 1
    • Section 2
    • Section 3
    `); - }); - - it('should support attribute translations on removed elements', () => { - // Translated template: - //
    - // trad {{exp1}} - //
    - - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 5, - vars: 5, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nAttributes(1, MSG_DIV_1_ATTR_1); - ɵɵi18nStart(2, MSG_DIV_1); - { - ɵɵelementStart(3, 'b'); // Will be removed - { ɵɵi18nAttributes(4, MSG_DIV_1_ATTR_1); } - ɵɵelementEnd(); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(`
    trad 1
    `); - }); - - it('should work with directives and host bindings', () => { - let directiveInstances: Directive[] = []; - - class Directive { - // @HostBinding('className') - klass = 'foo'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => { - const instance = new Directive(); - directiveInstances.push(instance); - return instance; - }, - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'className', ɵɵbind(ctx.klass), null, true); - } - } - }); - } - - // Translated template: - //
    - // trad {�0�, plural, - // =0 {no emails!} - // =1 {one email} - // other {�0� emails} - // } - //
    - - const MSG_DIV_1 = `trad {�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; - - class MyApp { - exp1 = 1; - exp2 = 2; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 6, - vars: 5, - directives: [Directive], - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [AttributeMarker.Bindings, 'dir']); - { - ɵɵi18nAttributes(1, MSG_DIV_1_ATTR_1); - ɵɵi18nStart(2, MSG_DIV_1); - { - ɵɵelementStart(3, 'b', [AttributeMarker.Bindings, 'dir']); // Will be removed - { ɵɵi18nAttributes(4, MSG_DIV_1_ATTR_1); } - ɵɵelementEnd(); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - ɵɵelement(5, 'div', [AttributeMarker.Bindings, 'dir']); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html) - .toEqual( - `
    trad one email
    `); - - directiveInstances.forEach(instance => instance.klass = 'bar'); - fixture.component.exp1 = 2; - fixture.component.exp2 = 3; - fixture.update(); - expect(fixture.html) - .toEqual( - `
    trad 2 emails
    `); - }); - - it('should fix the links when adding/moving/removing nodes', () => { - const MSG_DIV = `�#2��/#2��#8��/#8��#4��/#4��#5��/#5�Hello World�#3��/#3��#7��/#7�`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV); - { - ɵɵelement(2, 'div2'); - ɵɵelement(3, 'div3'); - ɵɵelement(4, 'div4'); - ɵɵelement(5, 'div5'); - ɵɵelement(6, 'div6'); - ɵɵelement(7, 'div7'); - ɵɵelement(8, 'div8'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - }, null, 9); - - expect(fixture.html) - .toEqual( - '
    Hello World
    '); - - const div0 = getTNode(0, fixture.hostView); - const div2 = getTNode(2, fixture.hostView); - const div3 = getTNode(3, fixture.hostView); - const div4 = getTNode(4, fixture.hostView); - const div5 = getTNode(5, fixture.hostView); - const div7 = getTNode(7, fixture.hostView); - const div8 = getTNode(8, fixture.hostView); - const text = getTNode(9, fixture.hostView); - expect(div0.child).toEqual(div2); - expect(div0.next).toBeNull(); - expect(div2.next).toEqual(div8); - expect(div8.next).toEqual(div4); - expect(div4.next).toEqual(div5); - expect(div5.next).toEqual(text); - expect(text.next).toEqual(div3); - expect(div3.next).toEqual(div7); - expect(div7.next).toBeNull(); - }); - - describe('projection', () => { - it('should project the translations', () => { - @Component({selector: 'child', template: '

    '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'p'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Je suis projeté depuis �#3��0��/#3��/#2�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
    - - I am projected from - {{name}} - - - - - -
    ` - // Translated to: - //
    - // - //

    - // Je suis projeté depuis {{name}} - //

    - //
    - //
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 8, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelementStart(2, 'child'); - { - ɵɵelementStart(3, 'b'); - { - ɵɵi18nAttributes(4, MSG_ATTR_1); - ɵɵelement(5, 'remove-me-1'); - } - ɵɵelementEnd(); - ɵɵelement(6, 'remove-me-2'); - } - ɵɵelementEnd(); - ɵɵelement(7, 'remove-me-3'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

    Je suis projeté depuis Parent

    '); - //

    Parent

    - //

    Je suis projeté depuis Parent

    - }); - - it('should project a translated i18n block', () => { - @Component({selector: 'child', template: '

    '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'p'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Je suis projeté depuis �0�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
    - - - I am projected from {{name}} - - -
    ` - // Translated to: - //
    - // - // - // Je suis projeté depuis {{name}} - // - // - //
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 7, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'child'); - { - ɵɵelement(2, 'any'); - ɵɵelementStart(3, 'b'); - { - ɵɵi18nAttributes(4, MSG_ATTR_1); - ɵɵi18n(5, MSG_DIV_SECTION_1); - } - ɵɵelementEnd(); - ɵɵelement(6, 'any'); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(4); - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

    Je suis projeté depuis Parent

    '); - - // it should be able to render a new component with the same template code - const fixture2 = new ComponentFixture(Parent); - expect(fixture2.html).toEqual(fixture.html); - - // Updating the fixture should work - fixture2.component.name = 'Parent 2'; - fixture.update(); - fixture2.update(); - expect(fixture2.html) - .toEqual( - '

    Je suis projeté depuis Parent 2

    '); - - // The first fixture should not have changed - expect(fixture.html) - .toEqual( - '

    Je suis projeté depuis Parent

    '); - }); - - it('should re-project translations when multiple projections', () => { - @Component({selector: 'grand-child', template: '
    '}) - class GrandChild { - static ngComponentDef = ɵɵdefineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'grand-child'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Bonjour�/#2� Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
    Bonjour Monde!
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { ɵɵelement(2, 'b'); } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
    Bonjour Monde!
    '); - //
    Bonjour
    - //
    Bonjour Monde!
    - }); - - xit('should re-project translations when removed placeholders', () => { - @Component({selector: 'grand-child', template: '
    '}) - class GrandChild { - static ngComponentDef = ɵɵdefineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'grand-child'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Bonjour Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
    Bonjour Monde!
    - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelement(2, 'b'); // will be removed - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
    Bonjour Monde!
    '); - }); - - it('should project translations with selectors', () => { - @Component({ - selector: 'child', - template: ` - - ` - }) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span']]]); - ɵɵprojection(0, 1); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Contenu�/#2�`; - - @Component({ - selector: 'parent', - template: ` - - - - - ` - // Translated to: - // Contenu - }) - class Parent { - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelement(2, 'span', ['title', 'keepMe']); - ɵɵelement(3, 'span', ['title', 'deleteMe']); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('Contenu'); - }); - }); - }); - describe('i18nPostprocess', () => { it('should handle valid cases', () => { const arr = ['�*1:1��#2:1�', '�#4:1�', '�6:1�', '�/#2:1��/*1:1�']; From 7dad3284e35764cafa720e36e5878f748f8e7d9c Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 25 Apr 2019 10:31:21 -0700 Subject: [PATCH 005/275] release: ts_api_guardian (#30120) PR Close #30120 --- tools/ts-api-guardian/README.md | 2 +- tools/ts-api-guardian/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/ts-api-guardian/README.md b/tools/ts-api-guardian/README.md index 19f343b4e7..296d4e1e6f 100644 --- a/tools/ts-api-guardian/README.md +++ b/tools/ts-api-guardian/README.md @@ -30,7 +30,7 @@ $ yarn bazel test //tools/ts-api-guardian:all Publish to NPM: ```sh -$ npm whoami # should be logged in as angular +$ yarn bazel run @nodejs//:npm whoami # should be logged in as angular $ grep version tools/ts-api-guardian/package.json # advance as needed $ yarn bazel run //tools/ts-api-guardian:ts-api-guardian.publish ``` diff --git a/tools/ts-api-guardian/package.json b/tools/ts-api-guardian/package.json index 79006d7179..9ea00518b4 100644 --- a/tools/ts-api-guardian/package.json +++ b/tools/ts-api-guardian/package.json @@ -1,12 +1,12 @@ { "name": "ts-api-guardian", - "version": "0.4.1", + "version": "0.4.4", "description": "Guards the API of TypeScript libraries!", "main": "lib/main.js", "typings": "lib/main.d.ts", "bazelWorkspaces": { "npm_ts_api_guardian": { - "version": "0.4.1", + "version": "0.4.4", "rootPath": "." } }, From 7569a2e0d9452e4ed7fa1fc504b1fd3d6dfcd8a7 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Fri, 10 May 2019 13:50:22 -0700 Subject: [PATCH 006/275] test(ivy): pure function acceptance tests (#30406) - Moves tests related to the pureFunction instructions from render3 to acceptance tests - Leaves behind one test for in-template javascript that is not supported syntax yet PR Close #30406 --- .../test/acceptance/pure_function_spec.ts | 482 ++++++++++++++++++ .../core/test/render3/pure_function_spec.ts | 481 +---------------- 2 files changed, 489 insertions(+), 474 deletions(-) create mode 100644 packages/core/test/acceptance/pure_function_spec.ts diff --git a/packages/core/test/acceptance/pure_function_spec.ts b/packages/core/test/acceptance/pure_function_spec.ts new file mode 100644 index 0000000000..01abb54b2e --- /dev/null +++ b/packages/core/test/acceptance/pure_function_spec.ts @@ -0,0 +1,482 @@ +/** + * @license + * Copyright Google Inc. 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 {CommonModule} from '@angular/common'; +import {Component, Input} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +describe('components using pure function instructions internally', () => { + describe('with array literals', () => { + @Component({ + selector: 'my-comp', + template: ``, + }) + class MyComp { + @Input() + names: string[] = []; + } + + + it('should support an array literal with a binding', () => { + @Component({ + template: ` + + `, + }) + class App { + showing = true; + customName = 'Carson'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + + const firstArray = myComp.names; + expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess']); + + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Carson', 'Bess']); + expect(firstArray).toBe(myComp.names); + + fixture.componentInstance.customName = 'Hannah'; + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Hannah', 'Bess']); + + // Identity must change if binding changes + expect(firstArray).not.toBe(myComp.names); + + // The property should not be set if the exp value is the same, so artificially + // setting the property to ensure it's not overwritten. + myComp.names = ['should not be overwritten']; + fixture.detectChanges(); + + expect(myComp !.names).toEqual(['should not be overwritten']); + }); + + + it('should support array literals in dynamic views', () => { + @Component({ + template: ` + + `, + }) + class App { + showing = true; + customName = 'Carson'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + expect(myComp.names).toEqual(['Nancy', 'Carson', 'Bess']); + }); + + it('should support multiple array literals passed through to one node', () => { + @Component({ + selector: 'many-prop-comp', + template: ``, + }) + class ManyPropComp { + @Input() + names1: string[] = []; + + @Input() + names2: string[] = []; + } + + @Component({ + template: ` + + + `, + }) + class App { + showing = true; + customName = 'Carson'; + customName2 = 'George'; + } + + TestBed.configureTestingModule({ + declarations: [App, ManyPropComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const manyPropComp = fixture.debugElement.query(By.directive(ManyPropComp)).componentInstance; + + expect(manyPropComp !.names1).toEqual(['Nancy', 'Carson']); + expect(manyPropComp !.names2).toEqual(['George']); + + fixture.componentInstance.customName = 'George'; + fixture.componentInstance.customName2 = 'Carson'; + fixture.detectChanges(); + expect(manyPropComp !.names1).toEqual(['Nancy', 'George']); + expect(manyPropComp !.names2).toEqual(['Carson']); + }); + + + it('should support an array literals inside fn calls', () => { + @Component({ + selector: 'parent-comp', + template: ` + + ` + }) + class ParentComp { + customName = 'Bess'; + + someFn(arr: string[]): string[] { + arr[0] = arr[0].toUpperCase(); + return arr; + } + } + + @Component({ + template: ` + + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp, ParentComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComps = + fixture.debugElement.queryAll(By.directive(MyComp)).map(f => f.componentInstance); + + + const firstArray = myComps[0].names; + const secondArray = myComps[1].names; + expect(firstArray).toEqual(['NANCY', 'Bess']); + expect(secondArray).toEqual(['NANCY', 'Bess']); + expect(firstArray).not.toBe(secondArray); + + fixture.detectChanges(); + expect(firstArray).toEqual(['NANCY', 'Bess']); + expect(secondArray).toEqual(['NANCY', 'Bess']); + expect(firstArray).toBe(myComps[0].names); + expect(secondArray).toBe(myComps[1].names); + }); + + + it('should support an array literal with more than 1 binding', () => { + @Component({ + template: ` + + `, + }) + class App { + showing = true; + customName = 'Carson'; + customName2 = 'Hannah'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + + const firstArray = myComp.names; + expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); + + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); + expect(firstArray).toBe(myComp.names); + + fixture.componentInstance.customName = 'George'; + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'George', 'Bess', 'Hannah']); + expect(firstArray).not.toBe(myComp.names); + + fixture.componentInstance.customName = 'Frank'; + fixture.componentInstance.customName2 = 'Ned'; + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Frank', 'Bess', 'Ned']); + + // The property should not be set if the exp value is the same, so artificially + // setting the property to ensure it's not overwritten. + myComp.names = ['should not be overwritten']; + fixture.detectChanges(); + expect(myComp.names).toEqual(['should not be overwritten']); + }); + + + it('should work up to 8 bindings', () => { + + @Component({ + template: ` + + + + + + + + + ` + }) + class App { + v1 = 'a'; + v2 = 'b'; + v3 = 'c'; + v4 = 'd'; + v5 = 'e'; + v6 = 'f'; + v7 = 'g'; + v8 = 'h'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const myComps = + fixture.debugElement.queryAll(By.directive(MyComp)).map(f => f.componentInstance); + + myComps.forEach(myComp => { + expect(myComp.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); + }); + + const app = fixture.componentInstance; + + app.v1 = 'a1'; + app.v2 = 'b1'; + app.v3 = 'c1'; + app.v4 = 'd1'; + app.v5 = 'e1'; + app.v6 = 'f1'; + app.v7 = 'g1'; + app.v8 = 'h1'; + + fixture.detectChanges(); + + expect(myComps[0].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h1']); + expect(myComps[1].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'h1']); + expect(myComps[2].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h1']); + expect(myComps[3].names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[4].names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[5].names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[6].names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[7].names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); + + app.v8 = 'h2'; + fixture.detectChanges(); + + expect(myComps[0].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h2']); + expect(myComps[1].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'h2']); + expect(myComps[2].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h2']); + expect(myComps[3].names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[4].names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[5].names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[6].names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[7].names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); + }); + + it('should work with pureFunctionV for 9+ bindings', () => { + @Component({ + template: ` + + + ` + }) + class App { + v0 = 'a'; + v1 = 'b'; + v2 = 'c'; + v3 = 'd'; + v4 = 'e'; + v5 = 'f'; + v6 = 'g'; + v7 = 'h'; + v8 = 'i'; + } + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + const app = fixture.componentInstance; + + expect(myComp.names).toEqual([ + 'start', 'a', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' + ]); + + app.v0 = 'a1'; + fixture.detectChanges(); + + expect(myComp.names).toEqual([ + 'start', 'a1', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' + ]); + + app.v4 = 'e5'; + fixture.detectChanges(); + + expect(myComp.names).toEqual([ + 'start', 'a1', 'b', 'c', 'd', 'modified_e5', 'f', 'g', 'h', 'i', 'end' + ]); + }); + }); + + describe('with object literals', () => { + @Component({ + selector: 'object-comp', + template: ``, + }) + class ObjectComp { + @Input() + config: any = []; + } + + it('should support an object literal', () => { + @Component({ + template: '', + }) + class App { + name = 'slide'; + } + + TestBed.configureTestingModule({ + declarations: [App, ObjectComp], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const objectComp = fixture.debugElement.query(By.directive(ObjectComp)).componentInstance; + + const firstObj = objectComp.config; + expect(objectComp.config).toEqual({duration: 500, animation: 'slide'}); + + fixture.detectChanges(); + expect(objectComp.config).toEqual({duration: 500, animation: 'slide'}); + expect(firstObj).toBe(objectComp.config); + + fixture.componentInstance.name = 'tap'; + fixture.detectChanges(); + expect(objectComp.config).toEqual({duration: 500, animation: 'tap'}); + + // Identity must change if binding changes + expect(firstObj).not.toBe(objectComp.config); + }); + + + it('should support expressions nested deeply in object/array literals', () => { + @Component({ + template: ` + + + `, + }) + class App { + name = 'slide'; + duration = 100; + } + + TestBed.configureTestingModule({ + declarations: [App, ObjectComp], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const objectComp = fixture.debugElement.query(By.directive(ObjectComp)).componentInstance; + + expect(objectComp.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] + }); + const firstConfig = objectComp.config; + + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] + }); + expect(objectComp.config).toBe(firstConfig); + + fixture.componentInstance.duration = 50; + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] + }); + expect(objectComp.config).not.toBe(firstConfig); + + fixture.componentInstance.name = 'tap'; + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'tap', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] + }); + + fixture.componentInstance.name = 'drag'; + fixture.componentInstance.duration = 500; + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'drag', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 500}] + }); + + // The property should not be set if the exp value is the same, so artificially + // setting the property to ensure it's not overwritten. + objectComp.config = ['should not be overwritten']; + fixture.detectChanges(); + expect(objectComp.config).toEqual(['should not be overwritten']); + }); + + it('should support multiple view instances with multiple bindings', () => { + @Component({ + template: ` + + + `, + }) + class App { + configs = [ + {opacity: 0, duration: 500}, + {opacity: 1, duration: 600}, + ]; + } + + TestBed.configureTestingModule({ + declarations: [App, ObjectComp], + imports: [CommonModule], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + const objectComps = + fixture.debugElement.queryAll(By.directive(ObjectComp)).map(f => f.componentInstance); + + expect(objectComps[0].config).toEqual({opacity: 0, duration: 500}); + expect(objectComps[1].config).toEqual({opacity: 1, duration: 600}); + + app.configs[0].duration = 1000; + fixture.detectChanges(); + expect(objectComps[0].config).toEqual({opacity: 0, duration: 1000}); + expect(objectComps[1].config).toEqual({opacity: 1, duration: 600}); + }); + }); +}); diff --git a/packages/core/test/render3/pure_function_spec.ts b/packages/core/test/render3/pure_function_spec.ts index 2824d8ee2a..42f7a17476 100644 --- a/packages/core/test/render3/pure_function_spec.ts +++ b/packages/core/test/render3/pure_function_spec.ts @@ -5,381 +5,13 @@ * 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 {AttributeMarker, ɵɵdefineComponent, ɵɵtemplate} from '../../src/render3/index'; -import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵnextContext} from '../../src/render3/instructions/all'; +import {ɵɵdefineComponent} from '../../src/render3/index'; +import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {ɵɵpureFunction1, ɵɵpureFunction2, ɵɵpureFunction3, ɵɵpureFunction4, ɵɵpureFunction5, ɵɵpureFunction6, ɵɵpureFunction7, ɵɵpureFunction8, ɵɵpureFunctionV} from '../../src/render3/pure_function'; -import {ComponentFixture, createComponent, getDirectiveOnNode, renderToHtml} from '../../test/render3/render_util'; - -import {NgIf} from './common_with_def'; - -describe('array literals', () => { - let myComp: MyComp; - - class MyComp { - // TODO(issue/24571): remove '!'. - names !: string[]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - selectors: [['my-comp']], - factory: function MyComp_Factory() { return myComp = new MyComp(); }, - consts: 0, - vars: 0, - template: function MyComp_Template(rf: RenderFlags, ctx: MyComp) {}, - inputs: {names: 'names'} - }); - } - - const directives = [MyComp]; - - it('should support an array literal with a binding', () => { - const e0_ff = (v: any) => ['Nancy', v, 'Bess']; - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'names', ɵɵbind(ɵɵpureFunction1(1, e0_ff, ctx.customName))); - } - }, 1, 3, directives); - - const fixture = new ComponentFixture(App); - fixture.component.customName = 'Carson'; - fixture.update(); - const firstArray = myComp !.names; - expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess']); - - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess']); - expect(firstArray).toBe(myComp !.names); - - fixture.component.customName = 'Hannah'; - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Hannah', 'Bess']); - - // Identity must change if binding changes - expect(firstArray).not.toBe(myComp !.names); - - // The property should not be set if the exp value is the same, so artificially - // setting the property to ensure it's not overwritten. - myComp !.names = ['should not be overwritten']; - fixture.update(); - expect(myComp !.names).toEqual(['should not be overwritten']); - }); - - it('should support array literals in dynamic views', () => { - const e0_ff = (v: any) => ['Nancy', v, 'Bess']; - - function IfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - const comp = ɵɵnextContext(); - ɵɵelementProperty(0, 'names', ɵɵbind(ɵɵpureFunction1(1, e0_ff, comp.customName))); - } - } - - /** - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, IfTemplate, 1, 3, 'my-comp', - [AttributeMarker.Bindings, 'names', AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - } - }, 1, 1, [MyComp, NgIf]); - - const fixture = new ComponentFixture(App); - fixture.component.showing = true; - fixture.component.customName = 'Carson'; - fixture.update(); - - expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess']); - }); - - it('should support multiple array literals passed through to one node', () => { - let manyPropComp: ManyPropComp; - - class ManyPropComp { - // TODO(issue/24571): remove '!'. - names1 !: string[]; - // TODO(issue/24571): remove '!'. - names2 !: string[]; - - static ngComponentDef = ɵɵdefineComponent({ - type: ManyPropComp, - selectors: [['many-prop-comp']], - factory: function ManyPropComp_Factory() { return manyPropComp = new ManyPropComp(); }, - consts: 0, - vars: 0, - template: function ManyPropComp_Template(rf: RenderFlags, ctx: ManyPropComp) {}, - inputs: {names1: 'names1', names2: 'names2'} - }); - } - - const e0_ff = (v: any) => ['Nancy', v]; - const e0_ff_1 = (v: any) => [v]; - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'many-prop-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'names1', ɵɵbind(ɵɵpureFunction1(2, e0_ff, ctx.customName))); - ɵɵelementProperty(0, 'names2', ɵɵbind(ɵɵpureFunction1(4, e0_ff_1, ctx.customName2))); - } - }, 1, 6, [ManyPropComp]); - - const fixture = new ComponentFixture(App); - fixture.component.customName = 'Carson'; - fixture.component.customName2 = 'George'; - fixture.update(); - expect(manyPropComp !.names1).toEqual(['Nancy', 'Carson']); - expect(manyPropComp !.names2).toEqual(['George']); - - fixture.component.customName = 'George'; - fixture.component.customName2 = 'Carson'; - fixture.update(); - expect(manyPropComp !.names1).toEqual(['Nancy', 'George']); - expect(manyPropComp !.names2).toEqual(['Carson']); - }); - - it('should support an array literals inside fn calls', () => { - let myComps: MyComp[] = []; - - const e0_ff = (v: any) => ['Nancy', v]; - - /** */ - class ParentComp { - customName = 'Bess'; - - someFn(arr: string[]): string[] { - arr[0] = arr[0].toUpperCase(); - return arr; - } - - static ngComponentDef = ɵɵdefineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - factory: () => new ParentComp(), - consts: 1, - vars: 3, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'my-comp'); - myComps.push(getDirectiveOnNode(0)); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'names', ɵɵbind(ctx.someFn(ɵɵpureFunction1(1, e0_ff, ctx.customName)))); - } - }, - directives: directives - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent-comp'); - ɵɵelement(1, 'parent-comp'); - } - }, 2, 0, [ParentComp]); - - const fixture = new ComponentFixture(App); - const firstArray = myComps[0].names; - const secondArray = myComps[1].names; - expect(firstArray).toEqual(['NANCY', 'Bess']); - expect(secondArray).toEqual(['NANCY', 'Bess']); - expect(firstArray).not.toBe(secondArray); - - fixture.update(); - expect(firstArray).toEqual(['NANCY', 'Bess']); - expect(secondArray).toEqual(['NANCY', 'Bess']); - expect(firstArray).toBe(myComps[0].names); - expect(secondArray).toBe(myComps[1].names); - }); - - it('should support an array literal with more than 1 binding', () => { - const e0_ff = (v1: any, v2: any) => ['Nancy', v1, 'Bess', v2]; - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'names', ɵɵbind(ɵɵpureFunction2(1, e0_ff, ctx.customName, ctx.customName2))); - } - }, 1, 4, directives); - - const fixture = new ComponentFixture(App); - fixture.component.customName = 'Carson'; - fixture.component.customName2 = 'Hannah'; - fixture.update(); - const firstArray = myComp !.names; - expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); - - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); - expect(firstArray).toBe(myComp !.names); - - fixture.component.customName = 'George'; - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'George', 'Bess', 'Hannah']); - expect(firstArray).not.toBe(myComp !.names); - - fixture.component.customName = 'Frank'; - fixture.component.customName2 = 'Ned'; - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Frank', 'Bess', 'Ned']); - - // The property should not be set if the exp value is the same, so artificially - // setting the property to ensure it's not overwritten. - myComp !.names = ['should not be overwritten']; - fixture.update(); - expect(myComp !.names).toEqual(['should not be overwritten']); - }); - - it('should work up to 8 bindings', () => { - let f3Comp: MyComp; - let f4Comp: MyComp; - let f5Comp: MyComp; - let f6Comp: MyComp; - let f7Comp: MyComp; - let f8Comp: MyComp; +import {ɵɵpureFunction2} from '../../src/render3/pure_function'; +import {getDirectiveOnNode, renderToHtml} from '../../test/render3/render_util'; - const e0_ff = (v1: any, v2: any, v3: any) => ['a', 'b', 'c', 'd', 'e', v1, v2, v3]; - const e2_ff = (v1: any, v2: any, v3: any, v4: any) => ['a', 'b', 'c', 'd', v1, v2, v3, v4]; - const e4_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any) => ['a', 'b', 'c', v1, v2, v3, v4, v5]; - const e6_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any, - v6: any) => ['a', 'b', v1, v2, v3, v4, v5, v6]; - const e8_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, - v7: any) => ['a', v1, v2, v3, v4, v5, v6, v7]; - const e10_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, - v8: any) => [v1, v2, v3, v4, v5, v6, v7, v8]; - - function Template(rf: RenderFlags, c: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'my-comp'); - f3Comp = getDirectiveOnNode(0); - ɵɵelementEnd(); - ɵɵelementStart(1, 'my-comp'); - f4Comp = getDirectiveOnNode(1); - ɵɵelementEnd(); - ɵɵelementStart(2, 'my-comp'); - f5Comp = getDirectiveOnNode(2); - ɵɵelementEnd(); - ɵɵelementStart(3, 'my-comp'); - f6Comp = getDirectiveOnNode(3); - ɵɵelementEnd(); - ɵɵelementStart(4, 'my-comp'); - f7Comp = getDirectiveOnNode(4); - ɵɵelementEnd(); - ɵɵelementStart(5, 'my-comp'); - f8Comp = getDirectiveOnNode(5); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'names', ɵɵbind(ɵɵpureFunction3(6, e0_ff, c[5], c[6], c[7]))); - ɵɵelementProperty(1, 'names', ɵɵbind(ɵɵpureFunction4(10, e2_ff, c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 2, 'names', ɵɵbind(ɵɵpureFunction5(15, e4_ff, c[3], c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 3, 'names', ɵɵbind(ɵɵpureFunction6(21, e6_ff, c[2], c[3], c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 4, 'names', - ɵɵbind(ɵɵpureFunction7(28, e8_ff, c[1], c[2], c[3], c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 5, 'names', - ɵɵbind(ɵɵpureFunction8(36, e10_ff, c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]))); - } - } - - renderToHtml(Template, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 6, 45, directives); - expect(f3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f5Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f6Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f7Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f8Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - - renderToHtml( - Template, ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1', 'i1'], 6, 45, directives); - expect(f3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h1']); - expect(f4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h1']); - expect(f5Comp !.names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h1']); - expect(f6Comp !.names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); - expect(f7Comp !.names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); - expect(f8Comp !.names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); - - renderToHtml( - Template, ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2', 'i1'], 6, 45, directives); - expect(f3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h2']); - expect(f4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h2']); - expect(f5Comp !.names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h2']); - expect(f6Comp !.names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); - expect(f7Comp !.names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); - expect(f8Comp !.names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); - }); - - it('should work with pureFunctionV for 9+ bindings', () => { - const e0_ff = - (v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, - v8: any) => ['start', v0, v1, v2, v3, v4, v5, v6, v7, v8, 'end']; - const e0_ff_1 = (v: any) => `modified_${v}`; - - /** - * - * - */ - function Template(rf: RenderFlags, c: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'names', ɵɵbind(ɵɵpureFunctionV(3, e0_ff, [ - c[0], c[1], c[2], c[3], ɵɵpureFunction1(1, e0_ff_1, c[4]), c[5], c[6], c[7], c[8] - ]))); - } - } - - renderToHtml(Template, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 1, 13, directives); - expect(myComp !.names).toEqual([ - 'start', 'a', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' - ]); - - renderToHtml(Template, ['a1', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 1, 13, directives); - expect(myComp !.names).toEqual([ - 'start', 'a1', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' - ]); - - renderToHtml(Template, ['a1', 'b', 'c', 'd', 'e5', 'f', 'g', 'h', 'i'], 1, 13, directives); - expect(myComp !.names).toEqual([ - 'start', 'a1', 'b', 'c', 'd', 'modified_e5', 'f', 'g', 'h', 'i', 'end' - ]); - }); - -}); describe('object literals', () => { let objectComp: ObjectComp; @@ -393,114 +25,15 @@ describe('object literals', () => { factory: function ObjectComp_Factory() { return objectComp = new ObjectComp(); }, consts: 0, vars: 1, - template: function ObjectComp_Template(rf: RenderFlags, ctx: ObjectComp) {}, + template: function ObjectComp_Template() {}, inputs: {config: 'config'} }); } const defs = [ObjectComp]; - it('should support an object literal', () => { - const e0_ff = (v: any) => { return {duration: 500, animation: v}; }; - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'object-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'config', ɵɵbind(ɵɵpureFunction1(1, e0_ff, ctx.name))); - } - }, 1, 3, defs); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'slide'; - fixture.update(); - const firstObj = objectComp !.config; - expect(objectComp !.config).toEqual({duration: 500, animation: 'slide'}); - - fixture.update(); - expect(objectComp !.config).toEqual({duration: 500, animation: 'slide'}); - expect(firstObj).toBe(objectComp !.config); - - fixture.component.name = 'tap'; - fixture.update(); - expect(objectComp !.config).toEqual({duration: 500, animation: 'tap'}); - - // Identity must change if binding changes - expect(firstObj).not.toBe(objectComp !.config); - }); - - it('should support expressions nested deeply in object/array literals', () => { - const e0_ff = (v1: any, v2: any) => { return {animation: v1, actions: v2}; }; - const e0_ff_1 = (v: any) => [{opacity: 0, duration: 0}, v]; - const e0_ff_2 = (v: any) => { return {opacity: 1, duration: v}; }; - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'object-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'config', - ɵɵbind(ɵɵpureFunction2( - 5, e0_ff, ctx.name, - ɵɵpureFunction1(3, e0_ff_1, ɵɵpureFunction1(1, e0_ff_2, ctx.duration))))); - } - }, 1, 8, defs); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'slide'; - fixture.component.duration = 100; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'slide', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] - }); - const firstConfig = objectComp !.config; - - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'slide', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] - }); - expect(objectComp !.config).toBe(firstConfig); - - fixture.component.duration = 50; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'slide', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] - }); - expect(objectComp !.config).not.toBe(firstConfig); - - fixture.component.name = 'tap'; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'tap', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] - }); - - fixture.component.name = 'drag'; - fixture.component.duration = 500; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'drag', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 500}] - }); - - // The property should not be set if the exp value is the same, so artificially - // setting the property to ensure it's not overwritten. - objectComp !.config = ['should not be overwritten']; - fixture.update(); - expect(objectComp !.config).toEqual(['should not be overwritten']); - }); - + // NOTE: This test cannot be ported to acceptance tests with TestBed because + // the syntax is still unsupported. it('should support multiple view instances with multiple bindings', () => { let objectComps: ObjectComp[] = []; From 31df5139c5e4b75234690c84ddeb7cac1bcfa75b Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 26 Apr 2019 15:29:27 +0200 Subject: [PATCH 007/275] test: fix several Bazel compiler tests in windows (#30146) ``` //packages/compiler-cli/test:ngc //packages/compiler/test:test ``` This also address `node_modules` to the ignored paths for ngc compiler as otherwise the `ready` is never fired Partially addresses #29785 PR Close #30146 --- .codefresh/codefresh.yml | 2 +- packages/compiler-cli/src/perform_watch.ts | 2 +- packages/compiler-cli/test/ngc_spec.ts | 8 +++++--- packages/compiler-cli/test/test_support.ts | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.codefresh/codefresh.yml b/.codefresh/codefresh.yml index 7316835c66..38cef7a59a 100644 --- a/.codefresh/codefresh.yml +++ b/.codefresh/codefresh.yml @@ -20,7 +20,7 @@ steps: # Add Bazel CI config - copy .codefresh\bazel.rc %ProgramData%\bazel.bazelrc # Run tests - - yarn bazel test //tools/ts-api-guardian:all //packages/language-service/test + - yarn bazel test //tools/ts-api-guardian:all //packages/language-service/test //packages/compiler/test //packages/compiler-cli/test:ngc - yarn test-ivy-aot //packages/animations/test //packages/common/test //packages/forms/test //packages/http/test //packages/platform-browser/test //packages/platform-browser-dynamic/test //packages/router/test - yarn bazel test //tools/public_api_guard/... - yarn bazel test //packages/compiler-cli/integrationtest:integrationtest //packages/compiler-cli/test/compliance:compliance diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index d070e729b2..0997dfd720 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -70,7 +70,7 @@ export function createPerformWatchHost( const watcher = chokidar.watch(options.basePath, { // ignore .dotfiles, .js and .map files. // can't ignore other files as we e.g. want to recompile if an `.html` file changes as well. - ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/, + ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json|node_modules)/, ignoreInitial: true, persistent: true, }); diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index cc2258882a..665bd37c98 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2028,11 +2028,13 @@ describe('ngc transformer command-line', () => { const exitCode = main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)); expect(exitCode).toBe(1, 'Compile was expected to fail'); + const srcPathWithSep = `lib${path.sep}`; expect(messages[0]) - .toEqual(`lib/test.component.ts(6,21): Error during template compile of 'TestComponent' + .toEqual( + `${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent' Tagged template expressions are not supported in metadata in 't1' - 't1' references 't2' at lib/indirect1.ts(3,27) - 't2' contains the error at lib/indirect2.ts(4,27). + 't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27) + 't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27). `); }); }); diff --git a/packages/compiler-cli/test/test_support.ts b/packages/compiler-cli/test/test_support.ts index 13a883eaf3..4e77479630 100644 --- a/packages/compiler-cli/test/test_support.ts +++ b/packages/compiler-cli/test/test_support.ts @@ -19,7 +19,7 @@ export function makeTempDir(): string { let dir: string; while (true) { const id = (Math.random() * 1000000).toFixed(0); - dir = path.join(tmpdir, `tmp.${id}`); + dir = path.posix.join(tmpdir, `tmp.${id}`); if (!fs.existsSync(dir)) break; } fs.mkdirSync(dir); From 2f35dbfd3b9f6d633ed272b67f177278a0541f6f Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 26 Apr 2019 16:14:05 +0200 Subject: [PATCH 008/275] test: fix ngtsc tests in windows (#30146) This commit fixes the following test target in windows ``` //packages/compiler-cli/test/ngtsc:ngtsc ``` PR Close #30146 --- .codefresh/codefresh.yml | 2 +- packages/compiler-cli/test/ngtsc/env.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.codefresh/codefresh.yml b/.codefresh/codefresh.yml index 38cef7a59a..2032ec4c5d 100644 --- a/.codefresh/codefresh.yml +++ b/.codefresh/codefresh.yml @@ -20,7 +20,7 @@ steps: # Add Bazel CI config - copy .codefresh\bazel.rc %ProgramData%\bazel.bazelrc # Run tests - - yarn bazel test //tools/ts-api-guardian:all //packages/language-service/test //packages/compiler/test //packages/compiler-cli/test:ngc + - yarn bazel test //tools/ts-api-guardian:all //packages/language-service/test //packages/compiler/test //packages/compiler-cli/test:ngc //packages/compiler-cli/test/ngtsc:ngtsc - yarn test-ivy-aot //packages/animations/test //packages/common/test //packages/forms/test //packages/http/test //packages/platform-browser/test //packages/platform-browser-dynamic/test //packages/router/test - yarn bazel test //tools/public_api_guard/... - yarn bazel test //packages/compiler-cli/integrationtest:integrationtest //packages/compiler-cli/test/compliance:compliance diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 52281fb454..98e5f4bd0e 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -29,7 +29,7 @@ function setupFakeCore(support: TestSupport): void { const nodeModulesPath = path.join(support.basePath, 'node_modules'); const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core'); - fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'dir'); + fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'junction'); } /** @@ -121,7 +121,7 @@ export class NgtscTestEnvironment { if (this.multiCompileHostExt === null) { throw new Error(`Not tracking written files - call enableMultipleCompilations()`); } - const outDir = path.join(this.support.basePath, 'built'); + const outDir = path.posix.join(this.support.basePath, 'built'); const writtenFiles = new Set(); this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => { if (rawFile.startsWith(outDir)) { From f74373f2dd829249e77537a90883eda833a8ce85 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 7 May 2019 22:57:55 -0400 Subject: [PATCH 009/275] fix(ivy): align NgModule registration timing with ViewEngine (#30244) Currently in Ivy `NgModule` registration happens when the class is declared, however this is inconsistent with ViewEngine and requires extra generated code. These changes remove the generated code for `registerModuleFactory`, pass the id through to the `ngModuleDef` and do the module registration inside `NgModuleFactory.create`. This PR resolves FW-1285. PR Close #30244 --- .../src/ngtsc/annotations/src/ng_module.ts | 6 +-- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 22 ++++---- .../compiler/src/compiler_facade_interface.ts | 1 + packages/compiler/src/jit_compiler_facade.ts | 1 + .../compiler/src/render3/r3_identifiers.ts | 3 -- .../src/render3/r3_module_compiler.ts | 13 ++++- packages/core/src/codegen_private_exports.ts | 2 +- .../src/compiler/compiler_facade_interface.ts | 1 + .../core/src/core_render3_private_export.ts | 7 ++- .../src/linker/ng_module_factory_loader.ts | 41 ++------------- .../linker/ng_module_factory_registration.ts | 52 +++++++++++++++++++ packages/core/src/metadata/ng_module.ts | 3 ++ packages/core/src/render3/definition.ts | 4 ++ packages/core/src/render3/jit/environment.ts | 3 -- packages/core/src/render3/jit/module.ts | 5 +- packages/core/src/render3/ng_module_ref.ts | 9 +++- .../test/linker/ng_module_integration_spec.ts | 14 ++++- tools/public_api_guard/core/core.d.ts | 1 + 18 files changed, 115 insertions(+), 73 deletions(-) create mode 100644 packages/core/src/linker/ng_module_factory_registration.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index f65b93db86..52040dfd1e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -162,6 +162,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { expect(jsContents).not.toContain('\u0275\u0275setNgModuleScope(TestModule,'); }); - it('should emit a \u0275registerNgModuleType call when the module has an id', () => { + it('should emit the id when the module\'s id is a string', () => { env.tsconfig(); env.write('test.ts', ` import {NgModule} from '@angular/core'; @@ -496,27 +496,27 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275registerNgModuleType(\'test\', TestModule);'); + expect(jsContents).toContain(`i0.\u0275\u0275defineNgModule({ type: TestModule, id: 'test' })`); }); - it('should emit a \u0275registerNgModuleType call when the module id is defined as `module.id`', - () => { - env.tsconfig(); - env.write('index.d.ts', ` + it('should emit the id when the module\'s id is defined as `module.id`', () => { + env.tsconfig(); + env.write('index.d.ts', ` declare const module = {id: string}; `); - env.write('test.ts', ` + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({id: module.id}) export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275registerNgModuleType(module.id, TestModule);'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents) + .toContain('i0.\u0275\u0275defineNgModule({ type: TestModule, id: module.id })'); + }); it('should filter out directives and pipes from module exports in the injector def', () => { env.tsconfig(); diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 7214522ddd..8fb8a44d07 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -106,6 +106,7 @@ export interface R3NgModuleMetadataFacade { exports: Function[]; emitInline: boolean; schemas: {name: string}[]|null; + id: string|null; } export interface R3InjectorMetadataFacade { diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 4e135ad90a..c14a4680bf 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -91,6 +91,7 @@ export class CompilerFacadeImpl implements CompilerFacade { emitInline: true, containsForwardDecls: false, schemas: facade.schemas ? facade.schemas.map(wrapReference) : null, + id: facade.id ? new WrappedNodeExpr(facade.id) : null, }; const res = compileNgModule(meta); return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 7e872d9a3e..af400aa98c 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -239,9 +239,6 @@ export class Identifiers { moduleName: CORE, }; - static registerNgModuleType: - o.ExternalReference = {name: 'ɵregisterNgModuleType', moduleName: CORE}; - // sanitization-related functions static sanitizeHtml: o.ExternalReference = {name: 'ɵɵsanitizeHtml', moduleName: CORE}; static sanitizeStyle: o.ExternalReference = {name: 'ɵɵsanitizeStyle', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_module_compiler.ts b/packages/compiler/src/render3/r3_module_compiler.ts index 213e762c5f..df83a468d6 100644 --- a/packages/compiler/src/render3/r3_module_compiler.ts +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -67,6 +67,9 @@ export interface R3NgModuleMetadata { * The set of schemas that declare elements to be allowed in the NgModule. */ schemas: R3Reference[]|null; + + /** Unique ID or expression representing the unique ID of an NgModule. */ + id: o.Expression|null; } /** @@ -81,7 +84,8 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { exports, schemas, containsForwardDecls, - emitInline + emitInline, + id } = meta; const additionalStatements: o.Statement[] = []; @@ -93,7 +97,8 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { declarations: o.Expression, imports: o.Expression, exports: o.Expression, - schemas: o.LiteralArrayExpr + schemas: o.LiteralArrayExpr, + id: o.Expression }; // Only generate the keys in the metadata if the arrays have values. @@ -130,6 +135,10 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { definitionMap.schemas = o.literalArr(schemas.map(ref => ref.value)); } + if (id) { + definitionMap.id = id; + } + const expression = o.importExpr(R3.defineNgModule).callFn([mapToMapExpression(definitionMap)]); const type = new o.ExpressionType(o.importExpr(R3.NgModuleDefWithMeta, [ new o.ExpressionType(moduleType), tupleTypeOf(declarations), tupleTypeOf(imports), diff --git a/packages/core/src/codegen_private_exports.ts b/packages/core/src/codegen_private_exports.ts index fffbd4f4fa..177d4560ab 100644 --- a/packages/core/src/codegen_private_exports.ts +++ b/packages/core/src/codegen_private_exports.ts @@ -7,5 +7,5 @@ */ export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver'; -export {registerModuleFactory as ɵregisterModuleFactory} from './linker/ng_module_factory_loader'; +export {registerModuleFactory as ɵregisterModuleFactory} from './linker/ng_module_factory_registration'; export {ArgumentType as ɵArgumentType, BindingFlags as ɵBindingFlags, DepFlags as ɵDepFlags, EMPTY_ARRAY as ɵEMPTY_ARRAY, EMPTY_MAP as ɵEMPTY_MAP, NodeFlags as ɵNodeFlags, QueryBindingType as ɵQueryBindingType, QueryValueType as ɵQueryValueType, ViewDefinition as ɵViewDefinition, ViewFlags as ɵViewFlags, anchorDef as ɵand, createComponentFactory as ɵccf, createNgModuleFactory as ɵcmf, createRendererType2 as ɵcrt, directiveDef as ɵdid, elementDef as ɵeld, getComponentViewDefinitionFactory as ɵgetComponentViewDefinitionFactory, inlineInterpolate as ɵinlineInterpolate, interpolate as ɵinterpolate, moduleDef as ɵmod, moduleProvideDef as ɵmpd, ngContentDef as ɵncd, nodeValue as ɵnov, pipeDef as ɵpid, providerDef as ɵprd, pureArrayDef as ɵpad, pureObjectDef as ɵpod, purePipeDef as ɵppd, queryDef as ɵqud, textDef as ɵted, unwrapValue as ɵunv, viewDef as ɵvid} from './view/index'; diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 7214522ddd..8fb8a44d07 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -106,6 +106,7 @@ export interface R3NgModuleMetadataFacade { exports: Function[]; emitInline: boolean; schemas: {name: string}[]|null; + id: string|null; } export interface R3InjectorMetadataFacade { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index c7374775ac..f2b48983e4 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -274,10 +274,9 @@ export { SWITCH_RENDERER2_FACTORY__POST_R3__ as ɵSWITCH_RENDERER2_FACTORY__POST_R3__, } from './render/api'; -export { - getModuleFactory__POST_R3__ as ɵgetModuleFactory__POST_R3__, - registerNgModuleType as ɵregisterNgModuleType, -} from './linker/ng_module_factory_loader'; +export { getModuleFactory__POST_R3__ as ɵgetModuleFactory__POST_R3__ } from './linker/ng_module_factory_loader'; + +export { registerNgModuleType as ɵregisterNgModuleType } from './linker/ng_module_factory_registration'; export { publishGlobalUtil as ɵpublishGlobalUtil, diff --git a/packages/core/src/linker/ng_module_factory_loader.ts b/packages/core/src/linker/ng_module_factory_loader.ts index f5a8b9aec6..2f6f710c6e 100644 --- a/packages/core/src/linker/ng_module_factory_loader.ts +++ b/packages/core/src/linker/ng_module_factory_loader.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Type} from '../interface/type'; import {NgModuleFactory as R3NgModuleFactory, NgModuleType} from '../render3/ng_module_ref'; -import {stringify} from '../util/stringify'; import {NgModuleFactory} from './ng_module_factory'; +import {getRegisteredNgModuleType} from './ng_module_factory_registration'; /** @@ -24,48 +23,14 @@ export abstract class NgModuleFactoryLoader { abstract load(path: string): Promise>; } -/** - * Map of module-id to the corresponding NgModule. - * - In pre Ivy we track NgModuleFactory, - * - In post Ivy we track the NgModuleType - */ -const modules = new Map|NgModuleType>(); - -/** - * Registers a loaded module. Should only be called from generated NgModuleFactory code. - * @publicApi - */ -export function registerModuleFactory(id: string, factory: NgModuleFactory) { - const existing = modules.get(id) as NgModuleFactory; - assertSameOrNotExisting(id, existing && existing.moduleType, factory.moduleType); - modules.set(id, factory); -} - -function assertSameOrNotExisting(id: string, type: Type| null, incoming: Type): void { - if (type && type !== incoming) { - throw new Error( - `Duplicate module registered for ${id} - ${stringify(type)} vs ${stringify(type.name)}`); - } -} - -export function registerNgModuleType(id: string, ngModuleType: NgModuleType) { - const existing = modules.get(id) as NgModuleType | null; - assertSameOrNotExisting(id, existing, ngModuleType); - modules.set(id, ngModuleType); -} - -export function clearModulesForTest(): void { - modules.clear(); -} - export function getModuleFactory__PRE_R3__(id: string): NgModuleFactory { - const factory = modules.get(id) as NgModuleFactory| null; + const factory = getRegisteredNgModuleType(id) as NgModuleFactory| null; if (!factory) throw noModuleError(id); return factory; } export function getModuleFactory__POST_R3__(id: string): NgModuleFactory { - const type = modules.get(id) as NgModuleType | null; + const type = getRegisteredNgModuleType(id) as NgModuleType | null; if (!type) throw noModuleError(id); return new R3NgModuleFactory(type); } diff --git a/packages/core/src/linker/ng_module_factory_registration.ts b/packages/core/src/linker/ng_module_factory_registration.ts new file mode 100644 index 0000000000..e63314323f --- /dev/null +++ b/packages/core/src/linker/ng_module_factory_registration.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google Inc. 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 {Type} from '../interface/type'; +import {NgModuleType} from '../render3/ng_module_ref'; +import {stringify} from '../util/stringify'; + +import {NgModuleFactory} from './ng_module_factory'; + + +/** + * Map of module-id to the corresponding NgModule. + * - In pre Ivy we track NgModuleFactory, + * - In post Ivy we track the NgModuleType + */ +const modules = new Map|NgModuleType>(); + +/** + * Registers a loaded module. Should only be called from generated NgModuleFactory code. + * @publicApi + */ +export function registerModuleFactory(id: string, factory: NgModuleFactory) { + const existing = modules.get(id) as NgModuleFactory; + assertSameOrNotExisting(id, existing && existing.moduleType, factory.moduleType); + modules.set(id, factory); +} + +function assertSameOrNotExisting(id: string, type: Type| null, incoming: Type): void { + if (type && type !== incoming) { + throw new Error( + `Duplicate module registered for ${id} - ${stringify(type)} vs ${stringify(type.name)}`); + } +} + +export function registerNgModuleType(id: string, ngModuleType: NgModuleType) { + const existing = modules.get(id) as NgModuleType | null; + assertSameOrNotExisting(id, existing, ngModuleType); + modules.set(id, ngModuleType); +} + +export function clearModulesForTest(): void { + modules.clear(); +} + +export function getRegisteredNgModuleType(id: string) { + return modules.get(id); +} diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index 36201f1726..0b7638f0ad 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -75,6 +75,9 @@ export interface NgModuleDef { /** The set of schemas that declare elements to be allowed in the NgModule. */ schemas: SchemaMetadata[]|null; + + /** Unique ID for the module with which it should be registered. */ + id: string|null; } /** diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index ecefde30c9..a5eb841056 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -366,6 +366,9 @@ export function ɵɵdefineNgModule(def: { /** The set of schemas that declare elements to be allowed in the NgModule. */ schemas?: SchemaMetadata[] | null; + + /** Unique ID for the module that is used with `getModuleFactory`. */ + id?: string | null; }): never { const res: NgModuleDef = { type: def.type, @@ -375,6 +378,7 @@ export function ɵɵdefineNgModule(def: { exports: def.exports || EMPTY_ARRAY, transitiveCompileScopes: null, schemas: def.schemas || null, + id: def.id || null, }; return res as never; } diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 3dc719945e..781ed99f83 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -9,7 +9,6 @@ import {ɵɵdefineInjectable, ɵɵdefineInjector,} from '../../di/interface/defs'; import {ɵɵinject} from '../../di/injector_compatibility'; import * as r3 from '../index'; -import {registerNgModuleType} from '../../linker/ng_module_factory_loader'; import * as sanitization from '../../sanitization/sanitization'; @@ -139,6 +138,4 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript, 'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl, 'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl, - - 'ɵregisterNgModuleType': registerNgModuleType, }; diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index 3777b0cf1c..324eef50e8 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -11,7 +11,6 @@ import {resolveForwardRef} from '../../di/forward_ref'; import {NG_INJECTOR_DEF} from '../../di/interface/defs'; import {reflectDependencies} from '../../di/jit/util'; import {Type} from '../../interface/type'; -import {registerNgModuleType} from '../../linker/ng_module_factory_loader'; import {Component} from '../../metadata'; import {ModuleWithProviders, NgModule, NgModuleDef, NgModuleTransitiveScopes} from '../../metadata/ng_module'; import {flatten} from '../../util/array_utils'; @@ -117,14 +116,12 @@ export function compileNgModuleDefs(moduleType: NgModuleType, ngModule: NgModule .map(expandModuleWithProviders), emitInline: true, schemas: ngModule.schemas ? flatten(ngModule.schemas) : null, + id: ngModule.id || null, }); } return ngModuleDef; } }); - if (ngModule.id) { - registerNgModuleType(ngModule.id, moduleType); - } let ngInjectorDef: any = null; Object.defineProperty(moduleType, NG_INJECTOR_DEF, { diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts index 889548a86b..07518d77f2 100644 --- a/packages/core/src/render3/ng_module_ref.ts +++ b/packages/core/src/render3/ng_module_ref.ts @@ -14,9 +14,11 @@ import {R3Injector, createInjector} from '../di/r3_injector'; import {Type} from '../interface/type'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; +import {registerNgModuleType} from '../linker/ng_module_factory_registration'; import {NgModuleDef} from '../metadata/ng_module'; import {assertDefined} from '../util/assert'; import {stringify} from '../util/stringify'; + import {ComponentFactoryResolver} from './component_ref'; import {getNgModuleDef} from './definition'; import {maybeUnwrapFn} from './util/misc_utils'; @@ -87,6 +89,11 @@ export class NgModuleFactory extends viewEngine_NgModuleFactory { constructor(public moduleType: Type) { super(); } create(parentInjector: Injector|null): viewEngine_NgModuleRef { - return new NgModuleRef(this.moduleType, parentInjector); + const moduleType = this.moduleType; + const moduleRef = new NgModuleRef(moduleType, parentInjector); + const ngModuleDef = getNgModuleDef(moduleType); + ngModuleDef && ngModuleDef.id && + registerNgModuleType(ngModuleDef.id, moduleType as NgModuleType); + return moduleRef; } } diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index 4500bd26df..50061efc78 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -17,7 +17,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {modifiedInIvy, obsoleteInIvy, onlyInIvy} from '@angular/private/testing'; import {InternalNgModuleRef, NgModuleFactory} from '../../src/linker/ng_module_factory'; -import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader'; +import {clearModulesForTest} from '../../src/linker/ng_module_factory_registration'; import {stringify} from '../../src/util/stringify'; class Engine {} @@ -327,6 +327,18 @@ function declareTests(config?: {useJit: boolean}) { createModule(SomeOtherModule); }).toThrowError(/Duplicate module registered/); }); + + it('should not throw immediately if two modules have the same id', () => { + expect(() => { + @NgModule({id: 'some-module'}) + class ModuleA { + } + + @NgModule({id: 'some-module'}) + class ModuleB { + } + }).not.toThrow(); + }); }); describe('entryComponents', () => { diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index ba7e436cf3..1a3c687730 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -777,6 +777,7 @@ export declare function ɵɵdefineNgModule(def: { imports?: Type[] | (() => Type[]); exports?: Type[] | (() => Type[]); schemas?: SchemaMetadata[] | null; + id?: string | null; }): never; export declare function ɵɵdefinePipe(pipeDef: { From 3a7bfc721efd61809ec16b950549f14ad4f9e75d Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 30 Apr 2019 13:44:17 +0200 Subject: [PATCH 010/275] fix(ivy): handle windows drives correctly (#30297) At the moment the module resolver will end up in an infinite loop in Windows because we are assuming that the root directory is always `/` however in windows this can be any drive letter example `c:/` or `d:/` etc... With this change we also resolve the drive letter in windows, when using `AbsoluteFsPath.from` for consistence so under `/foo` will be converted to `c:/foo` this is also needed because of relative paths with different drive letters. PR Close #30297 --- .../ngcc/src/dependencies/module_resolver.ts | 4 ++-- .../compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts | 2 +- .../compiler-cli/ngcc/test/helpers/mock_file_system.ts | 9 +++++---- packages/compiler-cli/src/ngtsc/path/src/types.ts | 9 +++++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts index e8c12e361d..c558a2bb97 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts @@ -118,7 +118,7 @@ export class ModuleResolver { */ private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { let folder = fromPath; - while (folder !== '/') { + while (!AbsoluteFsPath.isRoot(folder)) { folder = AbsoluteFsPath.dirname(folder); if (folder.endsWith('node_modules')) { // Skip up if the folder already ends in node_modules @@ -225,7 +225,7 @@ export class ModuleResolver { */ private findPackagePath(path: AbsoluteFsPath): AbsoluteFsPath|null { let folder = path; - while (folder !== '/') { + while (!AbsoluteFsPath.isRoot(folder)) { folder = AbsoluteFsPath.dirname(folder); if (this.fs.exists(AbsoluteFsPath.join(folder, 'package.json'))) { return folder; diff --git a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts index 19ddb626dd..d437a51945 100644 --- a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts +++ b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts @@ -27,7 +27,7 @@ export class NgccCompilerHost implements ts.CompilerHost { } getDefaultLibLocation(): string { - const nodeLibPath = AbsoluteFsPath.fromUnchecked(require.resolve('typescript')); + const nodeLibPath = AbsoluteFsPath.from(require.resolve('typescript')); return AbsoluteFsPath.join(nodeLibPath, '..'); } diff --git a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts index c47e2f359e..3e8fd219b4 100644 --- a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts +++ b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts @@ -14,7 +14,7 @@ import {FileStats, FileSystem} from '../../src/file_system/file_system'; export class MockFileSystem implements FileSystem { files: Folder = {}; constructor(...folders: Folder[]) { - folders.forEach(files => this.processFiles(this.files, files)); + folders.forEach(files => this.processFiles(this.files, files, true)); } exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path) !== null; } @@ -67,7 +67,7 @@ export class MockFileSystem implements FileSystem { return new MockFileStats(fileOrFolder); } - pwd(): AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked('/'); } + pwd(): AbsoluteFsPath { return AbsoluteFsPath.from('/'); } copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { this.writeFile(to, this.readFile(from)); @@ -82,9 +82,10 @@ export class MockFileSystem implements FileSystem { ensureDir(path: AbsoluteFsPath): void { this.ensureFolders(this.files, path.split('/')); } - private processFiles(current: Folder, files: Folder): void { + private processFiles(current: Folder, files: Folder, isRootPath = false): void { Object.keys(files).forEach(path => { - const segments = path.split('/'); + const pathResolved = isRootPath ? AbsoluteFsPath.from(path) : path; + const segments = pathResolved.split('/'); const lastSegment = segments.pop() !; const containingFolder = this.ensureFolders(current, segments); const entity = files[path]; diff --git a/packages/compiler-cli/src/ngtsc/path/src/types.ts b/packages/compiler-cli/src/ngtsc/path/src/types.ts index 6561d7e2fe..7286aeaaa7 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/types.ts +++ b/packages/compiler-cli/src/ngtsc/path/src/types.ts @@ -40,6 +40,12 @@ export const AbsoluteFsPath = { * Convert the path `str` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path. */ from: function(str: string): AbsoluteFsPath { + if (str.startsWith('/') && process.platform === 'win32') { + // in Windows if it's absolute path and starts with `/` we shall + // resolve it and return it including the drive. + str = path.resolve(str); + } + const normalized = normalizeSeparators(str); if (!isAbsolutePath(normalized)) { throw new Error(`Internal Error: AbsoluteFsPath.from(${str}): path is not absolute`); @@ -80,6 +86,9 @@ export const AbsoluteFsPath = { */ resolve: function(basePath: string, ...paths: string[]): AbsoluteFsPath { return AbsoluteFsPath.from(path.resolve(basePath, ...paths));}, + + /** Returns true when the path provided is the root path. */ + isRoot: function(path: AbsoluteFsPath): boolean { return AbsoluteFsPath.dirname(path) === path;}, }; /** From 1bd4891c9a9562f467c8d4679739f1a77c65ebb9 Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 30 Apr 2019 14:06:02 +0200 Subject: [PATCH 011/275] test: fix ngcc unit tests in windows (#30297) ``` //packages/compiler-cli/ngcc/test:test ``` Partially addresses #29785 PR Close #30297 --- .codefresh/codefresh.yml | 4 +--- packages/compiler-cli/ngcc/src/rendering/renderer.ts | 10 +++++++--- .../ngcc/test/dependencies/esm_dependency_host_spec.ts | 2 +- .../ngcc/test/packages/entry_point_spec.ts | 2 +- .../ngcc/test/writing/in_place_file_writer_spec.ts | 7 ++++--- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.codefresh/codefresh.yml b/.codefresh/codefresh.yml index 2032ec4c5d..fd74b0f32f 100644 --- a/.codefresh/codefresh.yml +++ b/.codefresh/codefresh.yml @@ -20,7 +20,5 @@ steps: # Add Bazel CI config - copy .codefresh\bazel.rc %ProgramData%\bazel.bazelrc # Run tests - - yarn bazel test //tools/ts-api-guardian:all //packages/language-service/test //packages/compiler/test //packages/compiler-cli/test:ngc //packages/compiler-cli/test/ngtsc:ngtsc - yarn test-ivy-aot //packages/animations/test //packages/common/test //packages/forms/test //packages/http/test //packages/platform-browser/test //packages/platform-browser-dynamic/test //packages/router/test - - yarn bazel test //tools/public_api_guard/... - - yarn bazel test //packages/compiler-cli/integrationtest:integrationtest //packages/compiler-cli/test/compliance:compliance + - yarn bazel test //tools/ts-api-guardian:all //tools/public_api_guard/... //packages/language-service/test //packages/compiler-cli/ngcc/test:test //packages/compiler-cli/integrationtest:integrationtest //packages/compiler-cli/test/compliance:compliance //packages/compiler/test //packages/compiler-cli/test:ngc //packages/compiler-cli/test/ngtsc:ngtsc diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index 911a8aac75..b1410eed1a 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -173,7 +173,7 @@ export abstract class Renderer { renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] { const input = this.extractSourceMap(dtsFile); const outputText = new MagicString(input.source); - const printer = ts.createPrinter(); + const printer = createPrinter(); const importManager = new ImportManager( this.getImportRewriter(this.bundle.dts !.r3SymbolsFile, false), IMPORT_PREFIX); @@ -476,7 +476,7 @@ export function mergeSourceMaps( */ export function renderConstantPool( sourceFile: ts.SourceFile, constantPool: ConstantPool, imports: ImportManager): string { - const printer = ts.createPrinter(); + const printer = createPrinter(); return constantPool.statements .map(stmt => translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER)) .map(stmt => printer.printNode(ts.EmitHint.Unspecified, stmt, sourceFile)) @@ -493,7 +493,7 @@ export function renderConstantPool( */ export function renderDefinitions( sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string { - const printer = ts.createPrinter(); + const printer = createPrinter(); const name = compiledClass.declaration.name; const translate = (stmt: Statement) => translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER); @@ -529,3 +529,7 @@ function getImportString( const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`; } + +function createPrinter(): ts.Printer { + return ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); +} \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts index bea7dc250f..0d10e82da0 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts @@ -70,7 +70,7 @@ describe('DependencyHost', () => { expect(dependencies.size).toBe(0); expect(missing.size).toBe(0); expect(deepImports.size).toBe(1); - expect(deepImports.has('/node_modules/lib-1/deep/import')).toBe(true); + expect(deepImports.has(_('/node_modules/lib-1/deep/import'))).toBe(true); }); it('should recurse into internal dependencies', () => { diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index 375fa013ac..c921fb38c2 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -12,7 +12,7 @@ import {getEntryPointInfo} from '../../src/packages/entry_point'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.fromUnchecked; +const _ = AbsoluteFsPath.from; describe('getEntryPointInfo()', () => { const SOME_PACKAGE = _('/some_package'); diff --git a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts index f3c13a01db..7e4ba0619a 100644 --- a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts @@ -11,7 +11,7 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.fromUnchecked; +const _ = AbsoluteFsPath.from; function createMockFileSystem() { return new MockFileSystem({ @@ -71,13 +71,14 @@ describe('InPlaceFileWriter', () => { it('should error if the backup file already exists', () => { const fs = createMockFileSystem(); const fileWriter = new InPlaceFileWriter(fs); + const absoluteBackupPath = _('/package/path/already-backed-up.js'); expect( () => fileWriter.writeBundle( {} as EntryPoint, {} as EntryPointBundle, [ - {path: _('/package/path/already-backed-up.js'), contents: 'MODIFIED BACKED UP'}, + {path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}, ])) .toThrowError( - 'Tried to overwrite /package/path/already-backed-up.js.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.'); + `Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`); }); }); From 18c0ba5272d9800da77c2c418b49f702a6799815 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 7 May 2019 10:08:21 +0200 Subject: [PATCH 012/275] test: fix ngcc integration tests in windows (#30297) ``` //packages/compiler-cli/ngcc/test:integration ``` Partially addresses #29785 PR Close #30297 --- .../ngcc/src/file_system/node_js_file_system.ts | 2 +- .../ngcc/src/packages/entry_point_finder.ts | 2 +- .../compiler-cli/ngcc/test/integration/ngcc_spec.ts | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts index 7f821c3986..8f4e7c797c 100644 --- a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts +++ b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts @@ -23,7 +23,7 @@ export class NodeJSFileSystem implements FileSystem { readdir(path: AbsoluteFsPath): PathSegment[] { return fs.readdirSync(path) as PathSegment[]; } lstat(path: AbsoluteFsPath): fs.Stats { return fs.lstatSync(path); } stat(path: AbsoluteFsPath): fs.Stats { return fs.statSync(path); } - pwd() { return AbsoluteFsPath.fromUnchecked(process.cwd()); } + pwd() { return AbsoluteFsPath.from(process.cwd()); } copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { cp(from, to); } moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { mv(from, to); } ensureDir(path: AbsoluteFsPath): void { mkdir('-p', path); } diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index f739669480..d1546a7466 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -92,7 +92,7 @@ export class EntryPointFinder { entryPoints.push(...this.getEntryPointsForPackage(packagePath)); // Also check for any nested node_modules in this package - const nestedNodeModulesPath = AbsoluteFsPath.resolve(packagePath, 'node_modules'); + const nestedNodeModulesPath = AbsoluteFsPath.join(packagePath, 'node_modules'); if (this.fs.exists(nestedNodeModulesPath)) { entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath)); } diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index f8c9023005..95e33310f3 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -7,8 +7,9 @@ */ import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; -import {existsSync, readFileSync, readdirSync, statSync, writeFileSync} from 'fs'; +import {existsSync, readFileSync, readdirSync, statSync, symlinkSync} from 'fs'; import * as mockFs from 'mock-fs'; +import * as path from 'path'; import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers'; import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; @@ -333,10 +334,15 @@ describe('ngcc main()', () => { function createMockFileSystem() { + const typeScriptPath = path.join(process.env.RUNFILES !, 'typescript'); + if (!existsSync(typeScriptPath)) { + symlinkSync(resolveNpmTreeArtifact('typescript'), typeScriptPath, 'junction'); + } + mockFs({ '/node_modules/@angular': loadAngularPackages(), - '/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs', 'index.js')), - '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib', 'tslib.js')), + '/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs')), + '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib')), '/node_modules/test-package': { 'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}', // no metadata.json file so not compiled by Angular. From 197584d1af1daeebe57a6b947851197553884e82 Mon Sep 17 00:00:00 2001 From: Jason Aden Date: Fri, 10 May 2019 08:49:17 -0700 Subject: [PATCH 013/275] fix(router): IE 11 bug can break URL unification when comparing objects (#30393) This PR fixes an issue where IE 11 can return `undefined` in with an `Object.keys` call. Solution is to add a runtime check on the value. Based on the types being passed, this shouldn't be necessary, but is needed only for IE 11. Unit test doesn't work for this PR because it can't be replicated easily. PR Close #30393 --- packages/router/src/utils/collection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/router/src/utils/collection.ts b/packages/router/src/utils/collection.ts index 6834cec4cf..149182dde2 100644 --- a/packages/router/src/utils/collection.ts +++ b/packages/router/src/utils/collection.ts @@ -23,7 +23,8 @@ export function shallowEqualArrays(a: any[], b: any[]): boolean { export function shallowEqual(a: {[x: string]: any}, b: {[x: string]: any}): boolean { const k1 = Object.keys(a); const k2 = Object.keys(b); - if (k1.length != k2.length) { + // IE 11 sometimes returns an `undefined` value here. This guard is for IE 11 only. + if (!(k1 || k2) || k1.length != k2.length) { return false; } let key: string; From a57f3e7bbf18f8a8a32de0916dd74051786e3169 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 13 May 2019 19:01:55 +0200 Subject: [PATCH 014/275] test(ivy): move render3 renderer_factory tests to acceptance (#30435) Moves all manual render3 tests which are located within the `renderer_factory_spec.ts` file to acceptance tests. A few tests that use Ivy-specific logic which is not replicable with `TestBed` remain in the render3 folder (e.g. using `renderTemplate`) Additionally migrated tests that assert the lifecycles of the renderer_factory are set to *ivy only* as the lifecycle seems to be different in Ivy. Tracked with: FW-1320 PR Close #30435 --- packages/core/test/acceptance/BUILD.bazel | 14 +- .../test/acceptance/renderer_factory_spec.ts | 200 ++++++++++++++++++ .../test/render3/renderer_factory_spec.ts | 126 +---------- 3 files changed, 216 insertions(+), 124 deletions(-) create mode 100644 packages/core/test/acceptance/renderer_factory_spec.ts diff --git a/packages/core/test/acceptance/BUILD.bazel b/packages/core/test/acceptance/BUILD.bazel index 8f74f3f99a..c037d23381 100644 --- a/packages/core/test/acceptance/BUILD.bazel +++ b/packages/core/test/acceptance/BUILD.bazel @@ -1,6 +1,6 @@ package(default_visibility = ["//visibility:private"]) -load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library", "ts_web_test_suite") ts_library( name = "acceptance_lib", @@ -9,6 +9,9 @@ ts_library( ["**/*.ts"], ), deps = [ + "//packages/animations", + "//packages/animations/browser", + "//packages/animations/browser/testing", "//packages/common", "//packages/compiler", "//packages/compiler/testing", @@ -17,7 +20,9 @@ ts_library( "//packages/core/testing", "//packages/platform-browser", "//packages/platform-browser-dynamic", + "//packages/platform-browser/animations", "//packages/platform-browser/testing", + "//packages/platform-server", "//packages/private/testing", "@npm//zone.js", ], @@ -34,3 +39,10 @@ jasmine_node_test( "@npm//zone.js", ], ) + +ts_web_test_suite( + name = "acceptance_web", + deps = [ + ":acceptance_lib", + ], +) diff --git a/packages/core/test/acceptance/renderer_factory_spec.ts b/packages/core/test/acceptance/renderer_factory_spec.ts new file mode 100644 index 0000000000..be26ca3543 --- /dev/null +++ b/packages/core/test/acceptance/renderer_factory_spec.ts @@ -0,0 +1,200 @@ +/** + * @license + * Copyright Google Inc. 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 {AnimationEvent} from '@angular/animations'; +import {ɵAnimationEngine, ɵNoopAnimationStyleNormalizer} from '@angular/animations/browser'; +import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; +import {DOCUMENT} from '@angular/common'; +import {Component, DoCheck, NgZone, RendererFactory2, RendererType2} from '@angular/core'; +import {NoopNgZone} from '@angular/core/src/zone/ng_zone'; +import {TestBed} from '@angular/core/testing'; +import {EventManager, ɵDomSharedStylesHost} from '@angular/platform-browser'; +import {ɵAnimationRendererFactory} from '@angular/platform-browser/animations'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {ServerRendererFactory2} from '@angular/platform-server/src/server_renderer'; +import {onlyInIvy} from '@angular/private/testing'; + +describe('renderer factory lifecycle', () => { + let logs: string[] = []; + + @Component({selector: 'some-component', template: `foo`}) + class SomeComponent implements DoCheck { + ngOnInit() { logs.push('some_component create'); } + ngDoCheck() { logs.push('some_component update'); } + } + + @Component({selector: 'some-component-with-error', template: `With error`}) + class SomeComponentWhichThrows { + ngOnInit() { throw new Error('SomeComponentWhichThrows threw'); } + } + + @Component({selector: 'lol', template: ``}) + class TestComponent implements DoCheck { + ngOnInit() { logs.push('test_component create'); } + ngDoCheck() { logs.push('test_component update'); } + } + + /** Creates a patched renderer factory that pushes entries to the test log */ + function createPatchedRendererFactory(document: any) { + let rendererFactory = getRendererFactory2(document); + const createRender = rendererFactory.createRenderer; + + rendererFactory.createRenderer = (hostElement: any, type: RendererType2 | null) => { + logs.push('create'); + return createRender.apply(rendererFactory, [hostElement, type]); + }; + + rendererFactory.begin = () => logs.push('begin'); + rendererFactory.end = () => logs.push('end'); + + return rendererFactory; + } + + beforeEach(() => { + logs = []; + + TestBed.configureTestingModule({ + declarations: [SomeComponent, SomeComponentWhichThrows, TestComponent], + providers: [{ + provide: RendererFactory2, + useFactory: (document: any) => createPatchedRendererFactory(document), + deps: [DOCUMENT] + }] + }); + }); + + onlyInIvy('FW-1320: Ivy creates renderer twice.').it('should work with a component', () => { + const fixture = TestBed.createComponent(SomeComponent); + fixture.detectChanges(); + expect(logs).toEqual( + ['create', 'create', 'begin', 'some_component create', 'some_component update', 'end']); + + logs = []; + fixture.detectChanges(); + expect(logs).toEqual(['begin', 'some_component update', 'end']); + }); + + onlyInIvy('FW-1320: Ivy creates renderer twice.') + .it('should work with a component which throws', () => { + expect(() => { + const fixture = TestBed.createComponent(SomeComponentWhichThrows); + fixture.detectChanges(); + }).toThrow(); + expect(logs).toEqual(['create', 'create', 'begin', 'end']); + }); +}); + +describe('animation renderer factory', () => { + let eventLogs: string[] = []; + let rendererFactory: RendererFactory2|null = null; + + function getAnimationLog(): MockAnimationPlayer[] { + return MockAnimationDriver.log as MockAnimationPlayer[]; + } + + beforeEach(() => { + eventLogs = []; + rendererFactory = null; + MockAnimationDriver.log = []; + + TestBed.configureTestingModule({ + declarations: [SomeComponentWithAnimation, SomeComponent], + providers: [{ + provide: RendererFactory2, + useFactory: (d: any) => rendererFactory = getAnimationRendererFactory2(d), + deps: [DOCUMENT] + }] + }); + }); + + @Component({ + selector: 'some-component', + template: ` +
    + foo +
    + `, + animations: [{ + type: 7, + name: 'myAnimation', + definitions: [{ + type: 1, + expr: '* => on', + animation: [{type: 4, styles: {type: 6, styles: {opacity: 1}, offset: null}, timings: 10}], + options: null + }], + options: {} + }] + }) + class SomeComponentWithAnimation { + exp: string|undefined; + + callback(event: AnimationEvent) { + eventLogs.push(`${event.fromState ? event.fromState : event.toState} - ${event.phaseName}`); + } + } + + @Component({selector: 'some-component', template: 'foo'}) + class SomeComponent { + } + + it('should work with components without animations', () => { + const fixture = TestBed.createComponent(SomeComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('foo'); + }); + + isBrowser && it('should work with animated components', (done) => { + const fixture = TestBed.createComponent(SomeComponentWithAnimation); + fixture.detectChanges(); + + expect(rendererFactory).toBeTruthy(); + expect(fixture.nativeElement.innerHTML) + .toMatch(/
    \s+foo\s+<\/div>/); + + fixture.componentInstance.exp = 'on'; + fixture.detectChanges(); + + const [player] = getAnimationLog(); + expect(player.keyframes).toEqual([ + {opacity: '*', offset: 0}, + {opacity: 1, offset: 1}, + ]); + player.finish(); + + rendererFactory !.whenRenderingDone !().then(() => { + expect(eventLogs).toEqual(['void - start', 'void - done', 'on - start', 'on - done']); + done(); + }); + }); +}); + +function getRendererFactory2(document: any): RendererFactory2 { + const fakeNgZone: NgZone = new NoopNgZone(); + const eventManager = new EventManager([], fakeNgZone); + const rendererFactory = new ServerRendererFactory2( + eventManager, fakeNgZone, document, new ɵDomSharedStylesHost(document)); + const origCreateRenderer = rendererFactory.createRenderer; + rendererFactory.createRenderer = function() { + const renderer = origCreateRenderer.apply(this, arguments); + renderer.destroyNode = () => {}; + return renderer; + }; + return rendererFactory; +} + +function getAnimationRendererFactory2(document: any): RendererFactory2 { + const fakeNgZone: NgZone = new NoopNgZone(); + return new ɵAnimationRendererFactory( + getRendererFactory2(document), + new ɵAnimationEngine( + document.body, new MockAnimationDriver(), new ɵNoopAnimationStyleNormalizer()), + fakeNgZone); +} diff --git a/packages/core/test/render3/renderer_factory_spec.ts b/packages/core/test/render3/renderer_factory_spec.ts index 4d3952b83c..1e7473bdbd 100644 --- a/packages/core/test/render3/renderer_factory_spec.ts +++ b/packages/core/test/render3/renderer_factory_spec.ts @@ -6,16 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationEvent} from '@angular/animations'; -import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; - import {RendererType2, ViewEncapsulation} from '../../src/core'; import {ɵɵdefineComponent} from '../../src/render3/index'; -import {tick, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all'; +import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2'; -import {TemplateFixture, containerEl, document, renderComponent, renderToHtml, toHtml} from './render_util'; +import {getRendererFactory2} from './imported_renderer2'; +import {TemplateFixture, document, renderToHtml} from './render_util'; describe('renderer factory lifecycle', () => { let logs: string[] = []; @@ -87,21 +84,6 @@ describe('renderer factory lifecycle', () => { beforeEach(() => { logs = []; }); - it('should work with a component', () => { - const component = renderComponent(SomeComponent, {rendererFactory}); - expect(logs).toEqual( - ['create', 'create', 'begin', 'component create', 'component update', 'end']); - - logs = []; - tick(component); - expect(logs).toEqual(['begin', 'component update', 'end']); - }); - - it('should work with a component which throws', () => { - expect(() => renderComponent(SomeComponentWhichThrows, {rendererFactory})).toThrow(); - expect(logs).toEqual(['create', 'create', 'begin', 'end']); - }); - it('should work with a template', () => { renderToHtml(Template, {}, 1, 0, null, null, rendererFactory); expect(logs).toEqual(['create', 'function create', 'function update']); @@ -125,108 +107,6 @@ describe('renderer factory lifecycle', () => { }); -describe('animation renderer factory', () => { - let eventLogs: string[] = []; - function getLog(): MockAnimationPlayer[] { - return MockAnimationDriver.log as MockAnimationPlayer[]; - } - - function resetLog() { MockAnimationDriver.log = []; } - - beforeEach(() => { - eventLogs = []; - resetLog(); - }); - - class SomeComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['some-component']], - consts: 1, - vars: 0, - template: function(rf: RenderFlags, ctx: SomeComponent) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'foo'); - } - }, - factory: () => new SomeComponent - }); - } - - class SomeComponentWithAnimation { - // TODO(issue/24571): remove '!'. - exp !: string; - callback(event: AnimationEvent) { - eventLogs.push(`${event.fromState ? event.fromState : event.toState} - ${event.phaseName}`); - } - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponentWithAnimation, - selectors: [['some-component']], - consts: 2, - vars: 1, - template: function(rf: RenderFlags, ctx: SomeComponentWithAnimation) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵlistener('@myAnimation.start', ctx.callback.bind(ctx)); - ɵɵlistener('@myAnimation.done', ctx.callback.bind(ctx)); - ɵɵtext(1, 'foo'); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, '@myAnimation', ɵɵbind(ctx.exp)); - } - }, - factory: () => new SomeComponentWithAnimation, - encapsulation: ViewEncapsulation.None, - styles: [], - data: { - animation: [{ - type: 7, - name: 'myAnimation', - definitions: [{ - type: 1, - expr: '* => on', - animation: - [{type: 4, styles: {type: 6, styles: {opacity: 1}, offset: null}, timings: 10}], - options: null - }], - options: {} - }] - }, - }); - } - - it('should work with components without animations', () => { - renderComponent(SomeComponent, {rendererFactory: getAnimationRendererFactory2(document)}); - expect(toHtml(containerEl)).toEqual('foo'); - }); - - isBrowser && it('should work with animated components', (done) => { - const rendererFactory = getAnimationRendererFactory2(document); - const component = renderComponent(SomeComponentWithAnimation, {rendererFactory}); - expect(toHtml(containerEl)) - .toMatch(/
    foo<\/div>/); - - component.exp = 'on'; - tick(component); - - const [player] = getLog(); - expect(player.keyframes).toEqual([ - {opacity: '*', offset: 0}, - {opacity: 1, offset: 1}, - ]); - player.finish(); - - rendererFactory.whenRenderingDone !().then(() => { - expect(eventLogs).toEqual(['void - start', 'void - done', 'on - start', 'on - done']); - done(); - }); - }); -}); - describe('Renderer2 destruction hooks', () => { const rendererFactory = getRendererFactory2(document); From d09d8e0adfb26e09ae82561e0f7769a407661a5a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 13 May 2019 12:01:15 +0200 Subject: [PATCH 015/275] test(ivy): move render3 directive tests to acceptance (#30432) Moves all manual render3 directive tests to TestBed acceptance tests. PR Close #30432 --- .../core/test/acceptance/directive_spec.ts | 171 +++++++++- packages/core/test/render3/directive_spec.ts | 316 ------------------ 2 files changed, 170 insertions(+), 317 deletions(-) delete mode 100644 packages/core/test/render3/directive_spec.ts diff --git a/packages/core/test/acceptance/directive_spec.ts b/packages/core/test/acceptance/directive_spec.ts index 6764573e7b..0fc8027586 100644 --- a/packages/core/test/acceptance/directive_spec.ts +++ b/packages/core/test/acceptance/directive_spec.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, EventEmitter, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {Input} from '@angular/core/src/metadata'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -16,6 +18,7 @@ describe('directives', () => { @Directive({selector: 'ng-template[test]'}) class TestDirective { + constructor(public templateRef: TemplateRef) {} } @Directive({selector: '[title]'}) @@ -26,6 +29,77 @@ describe('directives', () => { class TestComponent { } + it('should match directives with attribute selectors on bindings', () => { + @Directive({selector: '[test]'}) + class TestDir { + testValue: boolean|undefined; + + /** Setter to assert that a binding is not invoked with stringified attribute value */ + @Input() + set test(value: any) { + // Assert that the binding is processed correctly. The property should be set + // to a "false" boolean and never to the "false" string literal. + this.testValue = value; + if (value !== false) { + fail('Should only be called with a false Boolean value, got a non-falsy value'); + } + } + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate(TestComponent, ``); + + const fixture = TestBed.createComponent(TestComponent); + const testDir = fixture.debugElement.query(By.directive(TestDir)).injector.get(TestDir); + const spanEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + // the "test" attribute should not be reflected in the DOM as it is here only + // for directive matching purposes + expect(spanEl.hasAttribute('test')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('fade'); + expect(testDir.testValue).toBe(false); + }); + + it('should not accidentally set inputs from attributes extracted from bindings / outputs', + () => { + @Directive({selector: '[test]'}) + class TestDir { + @Input() prop1: boolean|undefined; + @Input() prop2: boolean|undefined; + testValue: boolean|undefined; + + /** Setter to assert that a binding is not invoked with stringified attribute value */ + @Input() + set test(value: any) { + // Assert that the binding is processed correctly. The property should be set + // to a "false" boolean and never to the "false" string literal. + this.testValue = value; + if (value !== false) { + fail('Should only be called with a false Boolean value, got a non-falsy value'); + } + } + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate( + TestComponent, + ``); + + const fixture = TestBed.createComponent(TestComponent); + const testDir = fixture.debugElement.query(By.directive(TestDir)).injector.get(TestDir); + const spanEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + // the "test" attribute should not be reflected in the DOM as it is here only + // for directive matching purposes + expect(spanEl.hasAttribute('test')).toBe(false); + expect(spanEl.hasAttribute('prop1')).toBe(false); + expect(spanEl.hasAttribute('prop2')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('fade'); + expect(testDir.testValue).toBe(false); + }); + it('should match directives on ng-template', () => { TestBed.configureTestingModule({declarations: [TestComponent, TestDirective]}); TestBed.overrideTemplate(TestComponent, ``); @@ -34,6 +108,8 @@ describe('directives', () => { const nodesWithDirective = fixture.debugElement.queryAllNodes(By.directive(TestDirective)); expect(nodesWithDirective.length).toBe(1); + expect(nodesWithDirective[0].injector.get(TestDirective).templateRef instanceof TemplateRef) + .toBe(true); }); it('should match directives on ng-template created by * syntax', () => { @@ -46,6 +122,32 @@ describe('directives', () => { expect(nodesWithDirective.length).toBe(1); }); + it('should match directives on ', () => { + @Directive({selector: 'ng-container[directiveA]'}) + class DirectiveA { + constructor(public viewContainerRef: ViewContainerRef) {} + } + + @Component({ + selector: 'my-component', + template: ` + + Some content + ` + }) + class MyComponent { + visible = true; + } + + TestBed.configureTestingModule( + {declarations: [MyComponent, DirectiveA], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + const directiveA = fixture.debugElement.query(By.css('span')).injector.get(DirectiveA); + + expect(directiveA.viewContainerRef).toBeTruthy(); + }); + it('should match directives on i18n-annotated attributes', () => { TestBed.configureTestingModule({declarations: [TestComponent, TitleDirective]}); TestBed.overrideTemplate(TestComponent, ` @@ -82,6 +184,73 @@ describe('directives', () => { expect(nodesWithDirective.length).toBe(0); }); + it('should match directives with attribute selectors on outputs', () => { + @Directive({selector: '[out]'}) + class TestDir { + @Output() out = new EventEmitter(); + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate(TestComponent, ``); + + const fixture = TestBed.createComponent(TestComponent); + const spanEl = fixture.nativeElement.children[0]; + + // "out" should not be part of reflected attributes + expect(spanEl.hasAttribute('out')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('span'); + expect(fixture.debugElement.query(By.directive(TestDir))).toBeTruthy(); + }); + + }); + + describe('outputs', () => { + @Directive({selector: '[out]'}) + class TestDir { + @Output() out = new EventEmitter(); + } + + it('should allow outputs of directive on ng-template', () => { + @Component({template: ``}) + class TestComp { + @ViewChild(TestDir) testDir: TestDir|undefined; + value = false; + } + + TestBed.configureTestingModule({declarations: [TestComp, TestDir]}); + const fixture = TestBed.createComponent(TestComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.testDir).toBeTruthy(); + expect(fixture.componentInstance.value).toBe(false); + + fixture.componentInstance.testDir !.out.emit(); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBe(true); + }); + + it('should allow outputs of directive on ng-container', () => { + @Component({ + template: ` + + Hello + ` + }) + class TestComp { + value = false; + } + + TestBed.configureTestingModule({declarations: [TestComp, TestDir]}); + const fixture = TestBed.createComponent(TestComp); + const testDir = fixture.debugElement.query(By.css('span')).injector.get(TestDir); + + expect(fixture.componentInstance.value).toBe(false); + + testDir.out.emit(); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeTruthy(); + }); + }); }); diff --git a/packages/core/test/render3/directive_spec.ts b/packages/core/test/render3/directive_spec.ts deleted file mode 100644 index b5bd88b269..0000000000 --- a/packages/core/test/render3/directive_spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {EventEmitter, TemplateRef, ViewContainerRef} from '@angular/core'; - -import {AttributeMarker, RenderFlags, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject} from '../../src/render3/index'; -import {ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵlistener, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; - -import {NgIf} from './common_with_def'; -import {ComponentFixture, TemplateFixture, createComponent} from './render_util'; - -describe('directive', () => { - - describe('selectors', () => { - - it('should match directives with attribute selectors on bindings', () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'test', '']], - factory: () => directiveInstance = new Directive, - inputs: {test: 'test', other: 'other'} - }); - - // TODO(issue/24571): remove '!'. - testValue !: boolean; - // TODO(issue/24571): remove '!'. - other !: boolean; - - /** - * A setter to assert that a binding is not invoked with stringified attribute value - */ - set test(value: any) { - // if a binding is processed correctly we should only be invoked with a false Boolean - // and never with the "false" string literal - this.testValue = value; - if (value !== false) { - fail('Should only be called with a false Boolean value, got a non-falsy value'); - } - } - } - - /** - * - */ - function createTemplate() { - // using 2 bindings to show example shape of attributes array - ɵɵelement(0, 'span', ['class', 'fade', AttributeMarker.Bindings, 'test', 'other']); - } - - function updateTemplate() { ɵɵelementProperty(0, 'test', ɵɵbind(false)); } - - const fixture = new TemplateFixture(createTemplate, updateTemplate, 1, 1, [Directive]); - - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html).toEqual(''); - expect(directiveInstance !.testValue).toBe(false); - }); - - it('should not accidentally set inputs from attributes extracted from bindings / outputs', - () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'test', '']], - factory: () => directiveInstance = new Directive, - inputs: {test: 'test', prop1: 'prop1', prop2: 'prop2'} - }); - - // TODO(issue/24571): remove '!'. - prop1 !: boolean; - // TODO(issue/24571): remove '!'. - prop2 !: boolean; - // TODO(issue/24571): remove '!'. - testValue !: boolean; - - - /** - * A setter to assert that a binding is not invoked with stringified attribute value - */ - set test(value: any) { - // if a binding is processed correctly we should only be invoked with a false Boolean - // and never with the "false" string literal - this.testValue = value; - if (value !== false) { - fail('Should only be called with a false Boolean value, got a non-falsy value'); - } - } - } - - /** - * - */ - function createTemplate() { - // putting name (test) in the "usual" value position - ɵɵelement( - 0, 'span', ['class', 'fade', AttributeMarker.Bindings, 'prop1', 'test', 'prop2']); - } - - function updateTemplate() { - ɵɵelementProperty(0, 'prop1', ɵɵbind(true)); - ɵɵelementProperty(0, 'test', ɵɵbind(false)); - ɵɵelementProperty(0, 'prop2', ɵɵbind(true)); - } - - const fixture = new TemplateFixture(createTemplate, updateTemplate, 1, 3, [Directive]); - - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html).toEqual(''); - expect(directiveInstance !.testValue).toBe(false); - }); - - it('should match directives on ', () => { - /** - * @Directive({ - * selector: 'ng-template[directiveA]' - * }) - * export class DirectiveA { - * constructor(public templateRef: TemplateRef) {} - * } - */ - let tmplRef: any; - class DirectiveA { - constructor(public templateRef: any) { tmplRef = templateRef; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['ng-template', 'directiveA', '']], - factory: () => new DirectiveA(ɵɵdirectiveInject(TemplateRef as any)) - }); - } - - function MyComponent_ng_template_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Some content'); - } - } - class MyComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - factory: () => new MyComponent(), - consts: 1, - vars: 0, - // Some content - template: function MyComponent_Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyComponent_ng_template_Template_0, 1, 0, 'ng-template', ['directiveA', '']); - } - }, - directives: [DirectiveA] - }); - } - - new ComponentFixture(MyComponent); - expect(tmplRef instanceof TemplateRef).toBeTruthy(); - }); - - it('should match directives on ', () => { - /** - * @Directive({ - * selector: 'ng-container[directiveA]' - * }) - * export class DirectiveA { - * constructor(public vcRef: ViewContainerRef) {} - * } - */ - let vcRef: any; - class DirectiveA { - constructor(public viewContainerRef: any) { vcRef = viewContainerRef; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['ng-container', 'directiveA', '']], - factory: () => new DirectiveA(ɵɵdirectiveInject(ViewContainerRef as any)) - }); - } - - function MyComponent_ng_container_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, ['directiveA', '']); - ɵɵtext(1, 'Some content'); - ɵɵelementContainerEnd(); - } - } - class MyComponent { - visible = true; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - factory: () => new MyComponent(), - consts: 1, - vars: 1, - // Some content - template: function MyComponent_Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyComponent_ng_container_Template_0, 2, 0, 'ng-container', - ['directiveA', '', AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.visible)); - } - }, - directives: [DirectiveA, NgIf] - }); - } - - new ComponentFixture(MyComponent); - expect(vcRef instanceof ViewContainerRef).toBeTruthy(); - }); - - it('should match directives with attribute selectors on outputs', () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'out', '']], - factory: () => directiveInstance = new Directive, - outputs: {out: 'out'} - }); - - out = new EventEmitter(); - } - - /** - * - */ - function createTemplate() { - ɵɵelementStart(0, 'span', [AttributeMarker.Bindings, 'out']); - { ɵɵlistener('out', () => {}); } - ɵɵelementEnd(); - } - - const fixture = new TemplateFixture(createTemplate, () => {}, 1, 0, [Directive]); - - // "out" should not be part of reflected attributes - expect(fixture.html).toEqual(''); - expect(directiveInstance !).not.toBeUndefined(); - }); - }); - - describe('outputs', () => { - - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'out', '']], - factory: () => directiveInstance = new Directive, - outputs: {out: 'out'} - }); - - out = new EventEmitter(); - } - - it('should allow outputs of directive on ng-template', () => { - /** - * - */ - const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'out']); - ɵɵlistener('out', () => { ctx.value = true; }); - } - }, 1, 0, [Directive]); - - const fixture = new ComponentFixture(Cmpt); - - expect(directiveInstance !).not.toBeUndefined(); - expect(fixture.component.value).toBeFalsy(); - - directiveInstance !.out.emit(); - fixture.update(); - expect(fixture.component.value).toBeTruthy(); - }); - - it('should allow outputs of directive on ng-container', () => { - /** - * - */ - const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, [AttributeMarker.Bindings, 'out']); - { - ɵɵlistener('out', () => { ctx.value = true; }); - } - ɵɵelementContainerEnd(); - } - }, 1, 0, [Directive]); - - const fixture = new ComponentFixture(Cmpt); - - expect(directiveInstance !).not.toBeUndefined(); - expect(fixture.component.value).toBeFalsy(); - - directiveInstance !.out.emit(); - fixture.update(); - expect(fixture.component.value).toBeTruthy(); - }); - - }); -}); From 090eac068a4cb17ab71eec7ce6c7e57f2643c2ab Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 12 May 2019 21:29:53 +0200 Subject: [PATCH 016/275] test(ivy): move property render3 tests to acceptance (#30426) Moves all manual render3 property binding tests to TestBed acceptance tests. Unfortunately three property binding tests could not be migrated as these rely on manual Ivy template code that is not supported within TestBed. These can be revisited as discussed in the framework team meeting. PR Close #30426 --- .../test/acceptance/property_binding_spec.ts | 465 ++++++++++++- packages/core/test/render3/properties_spec.ts | 621 ------------------ 2 files changed, 464 insertions(+), 622 deletions(-) delete mode 100644 packages/core/test/render3/properties_spec.ts diff --git a/packages/core/test/acceptance/property_binding_spec.ts b/packages/core/test/acceptance/property_binding_spec.ts index 1bfe65e1b7..8c96cfed80 100644 --- a/packages/core/test/acceptance/property_binding_spec.ts +++ b/packages/core/test/acceptance/property_binding_spec.ts @@ -5,11 +5,30 @@ * 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 {Component, Input} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, EventEmitter, Input, Output} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By, DomSanitizer, SafeUrl} from '@angular/platform-browser'; describe('property bindings', () => { + it('should support bindings to properties', () => { + @Component({template: ``}) + class Comp { + id: string|undefined; + } + + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + const spanEl = fixture.nativeElement.querySelector('span'); + + expect(spanEl.id).toBeFalsy(); + + fixture.componentInstance.id = 'testId'; + fixture.detectChanges(); + + expect(spanEl.id).toBe('testId'); + }); + it('should update bindings when value changes', () => { @Component({ template: ``, @@ -135,4 +154,448 @@ describe('property bindings', () => { expect(fixture.debugElement.query(By.css('input')).nativeElement.required).toBe(false); }); + + it('should support interpolation for properties', () => { + @Component({template: ``}) + class Comp { + id: string|undefined; + } + + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + const spanEl = fixture.nativeElement.querySelector('span'); + + fixture.componentInstance.id = 'testId'; + fixture.detectChanges(); + expect(spanEl.id).toBe('_testId_'); + + fixture.componentInstance.id = 'otherId'; + fixture.detectChanges(); + expect(spanEl.id).toBe('_otherId_'); + }); + + describe('input properties', () => { + @Directive({ + selector: '[myButton]', + }) + class MyButton { + @Input() disabled: boolean|undefined; + } + + @Directive({ + selector: '[otherDir]', + }) + class OtherDir { + @Input() id: number|undefined; + @Output('click') clickStream = new EventEmitter(); + } + + @Directive({ + selector: '[otherDisabledDir]', + }) + class OtherDisabledDir { + @Input() disabled: boolean|undefined; + } + + @Directive({ + selector: '[idDir]', + }) + class IdDir { + @Input('id') idNumber: string|undefined; + } + + it('should check input properties before setting (directives)', () => { + @Component({ + template: `` + }) + class App { + id = 0; + isDisabled = true; + } + + TestBed.configureTestingModule({declarations: [App, MyButton, OtherDir]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(MyButton)).injector.get(MyButton); + const otherDir = fixture.debugElement.query(By.directive(OtherDir)).injector.get(OtherDir); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('mybutton')).toBe(''); + expect(buttonEl.getAttribute('otherdir')).toBe(''); + expect(buttonEl.hasAttribute('id')).toBe(false); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(true); + expect(otherDir.id).toEqual(0); + + fixture.componentInstance.isDisabled = false; + fixture.componentInstance.id = 1; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('mybutton')).toBe(''); + expect(buttonEl.getAttribute('otherdir')).toBe(''); + expect(buttonEl.hasAttribute('id')).toBe(false); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(false); + expect(otherDir.id).toEqual(1); + }); + + it('should support mixed element properties and input properties', () => { + @Component({template: ``}) + class App { + isDisabled = true; + id = 0; + } + + TestBed.configureTestingModule({declarations: [App, MyButton]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(MyButton)).injector.get(MyButton); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('id')).toBe('0'); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(true); + + fixture.componentInstance.isDisabled = false; + fixture.componentInstance.id = 1; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('id')).toBe('1'); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(false); + }); + + it('should check that property is not an input property before setting (component)', () => { + @Component({ + selector: 'comp', + template: '', + }) + class Comp { + @Input() id: number|undefined; + } + + @Component({template: ``}) + class App { + id = 1; + } + + TestBed.configureTestingModule({declarations: [App, Comp]}); + const fixture = TestBed.createComponent(App); + const compDebugEl = fixture.debugElement.query(By.directive(Comp)); + fixture.detectChanges(); + + expect(compDebugEl.nativeElement.hasAttribute('id')).toBe(false); + expect(compDebugEl.componentInstance.id).toEqual(1); + + fixture.componentInstance.id = 2; + fixture.detectChanges(); + + expect(compDebugEl.nativeElement.hasAttribute('id')).toBe(false); + expect(compDebugEl.componentInstance.id).toEqual(2); + }); + + it('should support two input properties with the same name', () => { + @Component( + {template: ``}) + class App { + isDisabled = true; + } + + TestBed.configureTestingModule({declarations: [App, MyButton, OtherDisabledDir]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(MyButton)).injector.get(MyButton); + const otherDisabledDir = + fixture.debugElement.query(By.directive(OtherDisabledDir)).injector.get(OtherDisabledDir); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(true); + expect(otherDisabledDir.disabled).toEqual(true); + + fixture.componentInstance.isDisabled = false; + fixture.detectChanges(); + + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(false); + expect(otherDisabledDir.disabled).toEqual(false); + }); + + it('should set input property if there is an output first', () => { + @Component({ + template: ``, + }) + class App { + id = 1; + counter = 0; + onClick = () => this.counter++; + } + + TestBed.configureTestingModule({declarations: [App, OtherDir]}); + const fixture = TestBed.createComponent(App); + const otherDir = fixture.debugElement.query(By.directive(OtherDir)).injector.get(OtherDir); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.hasAttribute('id')).toBe(false); + expect(otherDir.id).toEqual(1); + + otherDir.clickStream.next(); + expect(fixture.componentInstance.counter).toEqual(1); + + fixture.componentInstance.id = 2; + fixture.detectChanges(); + expect(otherDir.id).toEqual(2); + }); + + it('should support unrelated element properties at same index in if-else block', () => { + @Component({ + template: ` + + + + ` + }) + class App { + condition = true; + id1 = 'one'; + id2 = 'two'; + id3 = 3; + } + + TestBed.configureTestingModule( + {declarations: [App, IdDir, OtherDir], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + let buttonElements = fixture.nativeElement.querySelectorAll('button'); + const idDir = fixture.debugElement.query(By.directive(IdDir)).injector.get(IdDir); + + expect(buttonElements.length).toBe(2); + expect(buttonElements[0].hasAttribute('id')).toBe(false); + expect(buttonElements[1].getAttribute('id')).toBe('two'); + expect(buttonElements[1].textContent).toBe('Click me too (2)'); + expect(idDir.idNumber).toBe('one'); + + fixture.componentInstance.condition = false; + fixture.componentInstance.id1 = 'four'; + fixture.detectChanges(); + + const otherDir = fixture.debugElement.query(By.directive(OtherDir)).injector.get(OtherDir); + buttonElements = fixture.nativeElement.querySelectorAll('button'); + expect(buttonElements.length).toBe(2); + expect(buttonElements[0].hasAttribute('id')).toBe(false); + expect(buttonElements[1].hasAttribute('id')).toBe(false); + expect(buttonElements[1].textContent).toBe('Click me too (3)'); + expect(idDir.idNumber).toBe('four'); + expect(otherDir.id).toBe(3); + }); + }); + + describe('attributes and input properties', () => { + + @Directive({selector: '[myDir]', exportAs: 'myDir'}) + class MyDir { + @Input() role: string|undefined; + @Input('dir') direction: string|undefined; + @Output('change') changeStream = new EventEmitter(); + } + + @Directive({selector: '[myDirB]'}) + class MyDirB { + @Input('role') roleB: string|undefined; + } + + it('should set input property based on attribute if existing', () => { + @Component({template: `
    `}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(divElement.getAttribute('mydir')).toBe(''); + expect(myDir.role).toEqual('button'); + }); + + it('should set input property and attribute if both defined', () => { + @Component({template: `
    `}) + class App { + role = 'listbox'; + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('listbox'); + + fixture.componentInstance.role = 'button'; + fixture.detectChanges(); + expect(myDir.role).toEqual('button'); + }); + + it('should set two directive input properties based on same attribute', () => { + @Component({template: `
    `}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir, MyDirB]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const myDirB = fixture.debugElement.query(By.directive(MyDirB)).injector.get(MyDirB); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('button'); + expect(myDirB.roleB).toEqual('button'); + }); + + it('should process two attributes on same directive', () => { + @Component({ + template: `
    `, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(divElement.getAttribute('dir')).toBe('rtl'); + expect(myDir.role).toEqual('button'); + expect(myDir.direction).toEqual('rtl'); + }); + + it('should process attributes and outputs properly together', () => { + @Component({template: `
    `}) + class App { + counter = 0; + onChange = () => this.counter++; + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('button'); + + myDir.changeStream.next(); + expect(fixture.componentInstance.counter).toEqual(1); + }); + + it('should process attributes properly for directives with later indices', () => { + @Component({ + template: ` +
    +
    + `, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir, MyDirB]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const myDirB = fixture.debugElement.query(By.directive(MyDirB)).injector.get(MyDirB); + const [buttonEl, listboxEl] = fixture.nativeElement.children; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('role')).toBe('button'); + expect(buttonEl.getAttribute('dir')).toBe('rtl'); + expect(listboxEl.getAttribute('role')).toBe('listbox'); + + expect(myDir.role).toEqual('button'); + expect(myDir.direction).toEqual('rtl'); + expect(myDirB.roleB).toEqual('listbox'); + }); + + it('should support attributes at same index inside an if-else block', () => { + @Component({ + template: ` +
    +
    +
    + `, + }) + class App { + condition = true; + } + + TestBed.configureTestingModule({declarations: [App, MyDir, MyDirB], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const myDirB = fixture.debugElement.query(By.directive(MyDirB)).injector.get(MyDirB); + let divElements = fixture.nativeElement.querySelectorAll('div'); + + expect(divElements.length).toBe(2); + expect(divElements[0].getAttribute('role')).toBe('listbox'); + expect(divElements[1].getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('listbox'); + expect(myDirB.roleB).toEqual('button'); + expect((myDirB as any).role).toBeUndefined(); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + + divElements = fixture.nativeElement.querySelectorAll('div'); + expect(divElements.length).toBe(2); + expect(divElements[0].getAttribute('role')).toBe('listbox'); + expect(divElements[1].getAttribute('role')).toBe('menu'); + expect(myDir.role).toEqual('listbox'); + expect(myDirB.roleB).toEqual('button'); + }); + + it('should process attributes properly inside a for loop', () => { + @Component({ + selector: 'comp', + template: `
    role: {{dir.role}}` + }) + class Comp { + } + + @Component({ + template: ` + + ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir, Comp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.children.length).toBe(2); + + const [comp1, comp2] = fixture.nativeElement.children; + + expect(comp1.tagName).toBe('COMP'); + expect(comp2.tagName).toBe('COMP'); + + expect(comp1.children[0].tagName).toBe('DIV'); + expect(comp1.children[0].getAttribute('role')).toBe('button'); + expect(comp1.textContent).toBe('role: button'); + + expect(comp2.children[0].tagName).toBe('DIV'); + expect(comp2.children[0].getAttribute('role')).toBe('button'); + expect(comp2.textContent).toBe('role: button'); + }); + + }); + }); diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts deleted file mode 100644 index d7efadec1a..0000000000 --- a/packages/core/test/render3/properties_spec.ts +++ /dev/null @@ -1,621 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {EventEmitter} from '@angular/core'; - -import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; -import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵlistener, ɵɵload, ɵɵreference, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; - -import {ComponentFixture, createComponent, renderToHtml} from './render_util'; - -describe('elementProperty', () => { - - it('should support bindings to properties', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.id = 'testId'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.id = 'otherId'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support creation time bindings to properties', () => { - function expensive(ctx: string): any { - if (ctx === 'cheapId') { - return ctx; - } else { - throw 'Too expensive!'; - } - } - - function Template(rf: RenderFlags, ctx: string) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - ɵɵelementProperty(0, 'id', expensive(ctx)); - } - } - - expect(renderToHtml(Template, 'cheapId', 1)).toEqual(''); - expect(renderToHtml(Template, 'expensiveId', 1)).toEqual(''); - }); - - it('should support interpolation for properties', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵinterpolation1('_', ctx.id, '_')); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.id = 'testId'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.id = 'otherId'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - describe('input properties', () => { - let button: MyButton; - let otherDir: OtherDir; - let otherDisabledDir: OtherDisabledDir; - let idDir: IdDir; - - class MyButton { - // TODO(issue/24571): remove '!'. - disabled !: boolean; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyButton, - selectors: [['', 'myButton', '']], - factory: () => button = new MyButton(), - inputs: {disabled: 'disabled'} - }); - } - - class OtherDir { - // TODO(issue/24571): remove '!'. - id !: number; - clickStream = new EventEmitter(); - - static ngDirectiveDef = ɵɵdefineDirective({ - type: OtherDir, - selectors: [['', 'otherDir', '']], - factory: () => otherDir = new OtherDir(), - inputs: {id: 'id'}, - outputs: {clickStream: 'click'} - }); - } - - class OtherDisabledDir { - // TODO(issue/24571): remove '!'. - disabled !: boolean; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: OtherDisabledDir, - selectors: [['', 'otherDisabledDir', '']], - factory: () => otherDisabledDir = new OtherDisabledDir(), - inputs: {disabled: 'disabled'} - }); - } - - class IdDir { - // TODO(issue/24571): remove '!'. - idNumber !: string; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: IdDir, - selectors: [['', 'idDir', '']], - factory: () => idDir = new IdDir(), - inputs: {idNumber: 'id'} - }); - } - - - const deps = [MyButton, OtherDir, OtherDisabledDir, IdDir]; - - it('should check input properties before setting (directives)', () => { - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['otherDir', '', 'myButton', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 2, 2, deps); - - const fixture = new ComponentFixture(App); - fixture.component.isDisabled = true; - fixture.component.id = 0; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(true); - expect(otherDir !.id).toEqual(0); - - fixture.component.isDisabled = false; - fixture.component.id = 1; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(false); - expect(otherDir !.id).toEqual(1); - }); - - it('should support mixed element properties and input properties', () => { - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['myButton', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 2, 2, deps); - - - const fixture = new ComponentFixture(App); - fixture.component.isDisabled = true; - fixture.component.id = 0; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(true); - - fixture.component.isDisabled = false; - fixture.component.id = 1; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(false); - }); - - it('should check that property is not an input property before setting (component)', () => { - let comp: Comp; - - class Comp { - // TODO(issue/24571): remove '!'. - id !: number; - - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - consts: 0, - vars: 0, - template: function(rf: RenderFlags, ctx: any) {}, - factory: () => comp = new Comp(), - inputs: {id: 'id'} - }); - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 1, 1, [Comp]); - - const fixture = new ComponentFixture(App); - fixture.component.id = 1; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(comp !.id).toEqual(1); - - fixture.component.id = 2; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(comp !.id).toEqual(2); - }); - - it('should support two input properties with the same name', () => { - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['myButton', '', 'otherDisabledDir', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - } - }, 2, 1, deps); - - const fixture = new ComponentFixture(App); - fixture.component.isDisabled = true; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(true); - expect(otherDisabledDir !.disabled).toEqual(true); - - fixture.component.isDisabled = false; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(false); - expect(otherDisabledDir !.disabled).toEqual(false); - }); - - it('should set input property if there is an output first', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['otherDir', '']); - { - ɵɵlistener('click', () => ctx.onClick()); - ɵɵtext(1, 'Click me'); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 2, 1, deps); - - const fixture = new ComponentFixture(App); - let counter = 0; - fixture.component.id = 1; - fixture.component.onClick = () => counter++; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(otherDir !.id).toEqual(1); - - otherDir !.clickStream.next(); - expect(counter).toEqual(1); - - fixture.component.id = 2; - fixture.update(); - fixture.html; - expect(otherDir !.id).toEqual(2); - }); - - it('should support unrelated element properties at same index in if-else block', () => { - /** - * // inputs: {'id': [0, 'idNumber']} - * % if (condition) { - * // inputs: null - * % } else { - * // inputs: {'id': [0, 'id']} - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['idDir', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - ɵɵcontainer(2); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id1)); - ɵɵcontainerRefreshStart(2); - { - if (ctx.condition) { - let rf0 = ɵɵembeddedViewStart(0, 2, 1); - if (rf0 & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { ɵɵtext(1, 'Click me too'); } - ɵɵelementEnd(); - } - if (rf0 & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id2)); - } - ɵɵembeddedViewEnd(); - } else { - let rf1 = ɵɵembeddedViewStart(1, 2, 1); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['otherDir', '']); - { ɵɵtext(1, 'Click me too'); } - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id3)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 1, deps); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.component.id1 = 'one'; - fixture.component.id2 = 'two'; - fixture.component.id3 = 3; - fixture.update(); - expect(fixture.html) - .toEqual(``); - expect(idDir !.idNumber).toEqual('one'); - - fixture.component.condition = false; - fixture.component.id1 = 'four'; - fixture.update(); - expect(fixture.html) - .toEqual(``); - expect(idDir !.idNumber).toEqual('four'); - expect(otherDir !.id).toEqual(3); - }); - - }); - - describe('attributes and input properties', () => { - let myDir: MyDir; - class MyDir { - // TODO(issue/24571): remove '!'. - role !: string; - // TODO(issue/24571): remove '!'. - direction !: string; - changeStream = new EventEmitter(); - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir, - selectors: [['', 'myDir', '']], - factory: () => myDir = new MyDir(), - inputs: {role: 'role', direction: 'dir'}, - outputs: {changeStream: 'change'}, - exportAs: ['myDir'] - }); - } - - let dirB: MyDirB; - class MyDirB { - // TODO(issue/24571): remove '!'. - roleB !: string; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDirB, - selectors: [['', 'myDirB', '']], - factory: () => dirB = new MyDirB(), - inputs: {roleB: 'role'} - }); - } - - const deps = [MyDir, MyDirB]; - - it('should set input property based on attribute if existing', () => { - - /**
    */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', '']); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
    `); - expect(myDir !.role).toEqual('button'); - }); - - it('should set input property and attribute if both defined', () => { - - /**
    */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'role', ɵɵbind(ctx.role)); - } - }, 1, 1, deps); - - const fixture = new ComponentFixture(App); - fixture.component.role = 'listbox'; - fixture.update(); - expect(fixture.html).toEqual(`
    `); - expect(myDir !.role).toEqual('listbox'); - - fixture.component.role = 'button'; - fixture.update(); - expect(myDir !.role).toEqual('button'); - }); - - it('should set two directive input properties based on same attribute', () => { - - /**
    */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', '', 'myDirB', '']); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
    `); - expect(myDir !.role).toEqual('button'); - expect(dirB !.roleB).toEqual('button'); - }); - - it('should process two attributes on same directive', () => { - - /**
    */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'dir', 'rtl', 'myDir', '']); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
    `); - expect(myDir !.role).toEqual('button'); - expect(myDir !.direction).toEqual('rtl'); - }); - - it('should process attributes and outputs properly together', () => { - - /**
    */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['role', 'button', 'myDir', '']); - { ɵɵlistener('change', () => ctx.onChange()); } - ɵɵelementEnd(); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - let counter = 0; - fixture.component.onChange = () => counter++; - fixture.update(); - expect(fixture.html).toEqual(`
    `); - expect(myDir !.role).toEqual('button'); - - myDir !.changeStream.next(); - expect(counter).toEqual(1); - }); - - it('should process attributes properly for directives with later indices', () => { - - /** - *
    - *
    - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'dir', 'rtl', 'myDir', '']); - ɵɵelement(1, 'div', ['role', 'listbox', 'myDirB', '']); - } - }, 2, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html) - .toEqual( - `
    `); - expect(myDir !.role).toEqual('button'); - expect(myDir !.direction).toEqual('rtl'); - expect(dirB !.roleB).toEqual('listbox'); - }); - - it('should support attributes at same index inside an if-else block', () => { - /** - *
    // initialInputs: [['role', 'listbox']] - * - * % if (condition) { - *
    // initialInputs: [['role', 'button']] - * % } else { - *
    // initialInputs: [null] - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'listbox', 'myDir', '']); - ɵɵcontainer(1); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(1); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDirB', '']); - } - ɵɵembeddedViewEnd(); - } else { - let rf2 = ɵɵembeddedViewStart(1, 1, 0); - if (rf2 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'menu']); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 2, 0, deps); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.update(); - expect(fixture.html) - .toEqual(`
    `); - expect(myDir !.role).toEqual('listbox'); - expect(dirB !.roleB).toEqual('button'); - expect((dirB !as any).role).toBeUndefined(); - - fixture.component.condition = false; - fixture.update(); - expect(fixture.html).toEqual(`
    `); - expect(myDir !.role).toEqual('listbox'); - }); - - it('should process attributes properly inside a for loop', () => { - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - consts: 3, - vars: 1, - /**
    {{ dir.role }} */ - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', ''], ['dir', 'myDir']); - ɵɵtext(2); - } - if (rf & RenderFlags.Update) { - const tmp = ɵɵreference(1) as any; - ɵɵtextBinding(2, ɵɵbind(tmp.role)); - } - }, - factory: () => new Comp(), - directives: () => [MyDir] - }); - } - - /** - * % for (let i = 0; i < 3; i++) { - * - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - for (let i = 0; i < 2; i++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, [Comp]); - - const fixture = new ComponentFixture(App); - expect(fixture.html) - .toEqual( - `
    button
    button
    `); - }); - - }); - -}); From 96baff3a856538f5b026b6b6ae703c08fb21b93a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 12 May 2019 18:52:41 +0200 Subject: [PATCH 017/275] test(ivy): move change detection tests into acceptance (#30425) Moves most of the r3 change detection tests into `acceptance`. Notes: * A handful of tests weren't migrated, because they were testing an API that isn't exposed publicly yet. * The `should throw if bindings in children of current view have changed` and `should NOT throw if bindings in ancestors of current view have changed` tests were removed, because there's not nice way of hitting the same code path with `TestBed` and doing the same assertion as with the raw instructions. I'm open to ideas on how we could do them. * There were a few tests that assert that the `innerHTML` looks in a particular way. I've switched them to use `textContent`, because Ivy and ViewEngine produce slightly different DOM. The tests were only checking whether the text has changed anyway. PR Close #30425 --- .../test/acceptance/change_detection_spec.ts | 854 +++++++++++- .../test/render3/change_detection_spec.ts | 1179 +---------------- 2 files changed, 863 insertions(+), 1170 deletions(-) diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts index 340a224321..596e67e2b7 100644 --- a/packages/core/test/acceptance/change_detection_spec.ts +++ b/packages/core/test/acceptance/change_detection_spec.ts @@ -7,7 +7,8 @@ */ -import {ApplicationRef, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ComponentRef, Directive, EmbeddedViewRef, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, Input, NgModule, OnInit, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -127,4 +128,853 @@ describe('change detection', () => { }); }); -}); \ No newline at end of file + describe('OnPush', () => { + @Component({ + selector: 'my-comp', + changeDetection: ChangeDetectionStrategy.OnPush, + template: `{{ doCheckCount }} - {{ name }} ` + }) + class MyComponent implements DoCheck { + @Input() + name = 'Nancy'; + doCheckCount = 0; + + ngDoCheck(): void { this.doCheckCount++; } + + onClick() {} + } + + @Component({selector: 'my-app', template: ''}) + class MyApp { + @ViewChild(MyComponent) comp !: MyComponent; + name: string = 'Nancy'; + } + + it('should check OnPush components on initialization', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + }); + + it('should call doCheck even when OnPush components are not dirty', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + fixture.detectChanges(); + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + + fixture.detectChanges(); + expect(fixture.componentInstance.comp.doCheckCount).toEqual(3); + }); + + it('should skip OnPush components in update mode when they are not dirty', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + // doCheckCount is 2, but 1 should be rendered since it has not been marked dirty. + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + fixture.detectChanges(); + + // doCheckCount is 3, but 1 should be rendered since it has not been marked dirty. + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + }); + + it('should check OnPush components in update mode when inputs change', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + fixture.componentInstance.name = 'Bess'; + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + // View should update, as changed input marks view dirty + expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Bess'); + + fixture.componentInstance.name = 'George'; + fixture.detectChanges(); + + // View should update, as changed input marks view dirty + expect(fixture.componentInstance.comp.doCheckCount).toEqual(3); + expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(4); + // View should not be updated to "4", as inputs have not changed. + expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George'); + }); + + it('should check OnPush components in update mode when component events occur', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + const button = fixture.nativeElement.querySelector('button') !; + button.click(); + + // No ticks should have been scheduled. + expect(fixture.componentInstance.comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + fixture.detectChanges(); + + // Because the onPush comp should be dirty, it should update once CD runs + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Nancy'); + }); + + it('should not check OnPush components in update mode when parent events occur', () => { + @Component({ + selector: 'button-parent', + template: '' + }) + class ButtonParent { + @ViewChild(MyComponent) comp !: MyComponent; + noop() {} + } + + TestBed.configureTestingModule({declarations: [MyComponent, ButtonParent]}); + const fixture = TestBed.createComponent(ButtonParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button#parent'); + button.click(); + fixture.detectChanges(); + + // The comp should still be clean. So doCheck will run, but the view should display 1. + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + }); + + it('should check parent OnPush components in update mode when child events occur', () => { + @Component({ + selector: 'button-parent', + template: '{{ doCheckCount }} - ', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class ButtonParent implements DoCheck { + @ViewChild(MyComponent) comp !: MyComponent; + noop() {} + + doCheckCount = 0; + ngDoCheck(): void { this.doCheckCount++; } + } + + @Component({selector: 'my-button-app', template: ''}) + class MyButtonApp { + @ViewChild(ButtonParent) parent !: ButtonParent; + } + + TestBed.configureTestingModule({declarations: [MyButtonApp, MyComponent, ButtonParent]}); + const fixture = TestBed.createComponent(MyButtonApp); + fixture.detectChanges(); + + const parent = fixture.componentInstance.parent; + const comp = parent.comp; + + expect(parent.doCheckCount).toEqual(1); + expect(comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy'); + + fixture.detectChanges(); + expect(parent.doCheckCount).toEqual(2); + // parent isn't checked, so child doCheck won't run + expect(comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy'); + + const button = fixture.nativeElement.querySelector('button'); + button.click(); + + // No ticks should have been scheduled. + expect(parent.doCheckCount).toEqual(2); + expect(comp.doCheckCount).toEqual(1); + + fixture.detectChanges(); + expect(parent.doCheckCount).toEqual(3); + expect(comp.doCheckCount).toEqual(2); + expect(fixture.nativeElement.textContent.trim()).toEqual('3 - 2 - Nancy'); + }); + + }); + + describe('ChangeDetectorRef', () => { + describe('detectChanges()', () => { + @Component({ + selector: 'my-comp', + template: '{{ name }}', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class MyComp implements DoCheck { + doCheckCount = 0; + name = 'Nancy'; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Component({selector: 'parent-comp', template: `{{ doCheckCount}} - `}) + class ParentComp implements DoCheck { + @ViewChild(MyComp) myComp !: MyComp; + + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Directive({selector: '[dir]'}) + class Dir { + constructor(public cdr: ChangeDetectorRef) {} + } + + it('should check the component view when called by component (even when OnPush && clean)', + () => { + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('Nancy'); + + fixture.componentInstance.name = + 'Bess'; // as this is not an Input, the component stays clean + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Bess'); + }); + + it('should NOT call component doCheck when called by a component', () => { + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.doCheckCount).toEqual(1); + + // NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you + // may see doCheck called in some cases bc of the extra CD run triggered by zone.js. + // It's important not to call doCheck to allow calls to detectChanges in that hook. + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(1); + }); + + it('should NOT check the component parent when called by a child component', () => { + TestBed.configureTestingModule({declarations: [MyComp, ParentComp]}); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('1 - Nancy'); + + fixture.componentInstance.doCheckCount = 100; + fixture.componentInstance.myComp.cdr.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(100); + expect(fixture.nativeElement.textContent).toEqual('1 - Nancy'); + }); + + it('should check component children when called by component if dirty or check-always', + () => { + TestBed.configureTestingModule({declarations: [MyComp, ParentComp]}); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(1); + + fixture.componentInstance.myComp.name = 'Bess'; + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(1); + expect(fixture.componentInstance.myComp.doCheckCount).toEqual(2); + // OnPush child is not dirty, so its change isn't rendered. + expect(fixture.nativeElement.textContent).toEqual('1 - Nancy'); + }); + + it('should not group detectChanges calls (call every time)', () => { + TestBed.configureTestingModule({declarations: [MyComp, ParentComp]}); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.doCheckCount).toEqual(1); + + fixture.componentInstance.cdr.detectChanges(); + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.componentInstance.myComp.doCheckCount).toEqual(3); + }); + + it('should check component view when called by directive on component node', () => { + @Component({template: ''}) + class MyApp { + @ViewChild(MyComp) myComp !: MyComp; + @ViewChild(Dir) dir !: Dir; + } + + TestBed.configureTestingModule({declarations: [MyComp, Dir, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('Nancy'); + + fixture.componentInstance.myComp.name = 'George'; + fixture.componentInstance.dir.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('George'); + }); + + it('should check host component when called by directive on element node', () => { + @Component({template: '{{ value }}
    '}) + class MyApp { + @ViewChild(MyComp) myComp !: MyComp; + @ViewChild(Dir) dir !: Dir; + value = ''; + } + + TestBed.configureTestingModule({declarations: [Dir, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + fixture.componentInstance.value = 'Frank'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Frank'); + + fixture.componentInstance.value = 'Joe'; + fixture.componentInstance.dir.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Joe'); + }); + + it('should check the host component when called from EmbeddedViewRef', () => { + @Component({template: '{{ name }}
    '}) + class MyApp { + @ViewChild(Dir) dir !: Dir; + showing = true; + name = 'Amelia'; + } + + TestBed.configureTestingModule({declarations: [Dir, MyApp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('Amelia'); + + fixture.componentInstance.name = 'Emerson'; + fixture.componentInstance.dir.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Emerson'); + }); + + it('should support call in ngOnInit', () => { + @Component({template: '{{ value }}'}) + class DetectChangesComp implements OnInit { + value = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.value++; + this.cdr.detectChanges(); + } + } + + TestBed.configureTestingModule({declarations: [DetectChangesComp]}); + const fixture = TestBed.createComponent(DetectChangesComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('1'); + }); + + ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => { + it(`should not go infinite loop when recursively called from children's ng${hook}`, () => { + @Component({template: ''}) + class ParentComp { + constructor(public cdr: ChangeDetectorRef) {} + triggerChangeDetection() { this.cdr.detectChanges(); } + } + + @Component({template: '{{inp}}', selector: 'child-comp'}) + class ChildComp { + @Input() + inp: any = ''; + + count = 0; + constructor(public parentComp: ParentComp) {} + + ngOnInit() { this.check('OnInit'); } + ngAfterContentInit() { this.check('AfterContentInit'); } + ngAfterViewInit() { this.check('AfterViewInit'); } + ngOnChanges() { this.check('OnChanges'); } + + check(h: string) { + if (h === hook) { + this.count++; + if (this.count > 1) throw new Error(`ng${hook} should be called only once!`); + this.parentComp.triggerChangeDetection(); + } + } + } + + TestBed.configureTestingModule({declarations: [ParentComp, ChildComp]}); + + expect(() => { + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + }).not.toThrow(); + }); + }); + + it('should support call in ngDoCheck', () => { + @Component({template: '{{doCheckCount}}'}) + class DetectChangesComp { + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { + this.doCheckCount++; + this.cdr.detectChanges(); + } + } + + TestBed.configureTestingModule({declarations: [DetectChangesComp]}); + const fixture = TestBed.createComponent(DetectChangesComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('1'); + }); + + describe('dynamic views', () => { + @Component({selector: 'structural-comp', template: '{{ value }}'}) + class StructuralComp { + @Input() + tmp !: TemplateRef; + value = 'one'; + + constructor(public vcr: ViewContainerRef) {} + + create() { return this.vcr.createEmbeddedView(this.tmp, {ctx: this}); } + } + + it('should support ViewRef.detectChanges()', () => { + @Component({ + template: + '{{ ctx.value }}' + }) + class App { + @ViewChild(StructuralComp) structuralComp !: StructuralComp; + } + + TestBed.configureTestingModule({declarations: [App, StructuralComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + const viewRef: EmbeddedViewRef = fixture.componentInstance.structuralComp.create(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('oneone'); + + // check embedded view update + fixture.componentInstance.structuralComp.value = 'two'; + viewRef.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('onetwo'); + + // check root view update + fixture.componentInstance.structuralComp.value = 'three'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('threethree'); + }); + + it('should support ViewRef.detectChanges() directly after creation', () => { + @Component({ + template: 'Template text' + }) + class App { + @ViewChild(StructuralComp) structuralComp !: StructuralComp; + } + + TestBed.configureTestingModule({declarations: [App, StructuralComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + const viewRef: EmbeddedViewRef = fixture.componentInstance.structuralComp.create(); + viewRef.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('oneTemplate text'); + }); + + }); + + }); + + describe('attach/detach', () => { + @Component({selector: 'detached-comp', template: '{{ value }}'}) + class DetachedComp implements DoCheck { + value = 'one'; + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Component({template: ''}) + class MyApp { + @ViewChild(DetachedComp) comp !: DetachedComp; + + constructor(public cdr: ChangeDetectorRef) {} + } + + it('should not check detached components', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.comp.cdr.detach(); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + }); + + it('should check re-attached components', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.comp.cdr.detach(); + fixture.componentInstance.comp.value = 'two'; + + fixture.componentInstance.comp.cdr.reattach(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + it('should call lifecycle hooks on detached components', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(1); + + fixture.componentInstance.comp.cdr.detach(); + + fixture.detectChanges(); + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + }); + + it('should check detached component when detectChanges is called', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.comp.cdr.detach(); + + fixture.componentInstance.comp.value = 'two'; + fixture.componentInstance.comp.cdr.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + it('should not check detached component when markDirty is called', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + const comp = fixture.componentInstance.comp; + + comp.cdr.detach(); + comp.value = 'two'; + comp.cdr.markForCheck(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + }); + + it('should detach any child components when parent is detached', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.cdr.detach(); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.cdr.reattach(); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + it('should detach OnPush components properly', () => { + + @Component({ + selector: 'on-push-comp', + template: '{{ value }}', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class OnPushComp { + @Input() + value !: string; + + constructor(public cdr: ChangeDetectorRef) {} + } + + @Component({template: ''}) + class OnPushApp { + @ViewChild(OnPushComp) onPushComp !: OnPushComp; + value = ''; + } + + TestBed.configureTestingModule({declarations: [OnPushApp, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushApp); + fixture.detectChanges(); + + fixture.componentInstance.value = 'one'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.onPushComp.cdr.detach(); + + fixture.componentInstance.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.onPushComp.cdr.reattach(); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + }); + + describe('markForCheck()', () => { + @Component({ + selector: 'on-push-comp', + template: '{{ value }}', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class OnPushComp implements DoCheck { + value = 'one'; + + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Component({ + template: '{{ value }} - ', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class OnPushParent { + @ViewChild(OnPushComp) comp !: OnPushComp; + value = 'one'; + } + + it('should ensure OnPush components are checked', () => { + TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.cdr.markForCheck(); + + // Change detection should not have run yet, since markForCheck + // does not itself schedule change detection. + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - two'); + }); + + it('should never schedule change detection on its own', () => { + TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushParent); + fixture.detectChanges(); + const comp = fixture.componentInstance.comp; + + expect(comp.doCheckCount).toEqual(1); + + comp.cdr.markForCheck(); + comp.cdr.markForCheck(); + + expect(comp.doCheckCount).toEqual(1); + }); + + it('should ensure ancestor OnPush components are checked', () => { + TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.cdr.markForCheck(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two - one'); + + }); + + it('should ensure OnPush components in embedded views are checked', () => { + @Component({ + template: '{{ value }} - ', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class EmbeddedViewParent { + @ViewChild(OnPushComp) comp !: OnPushComp; + value = 'one'; + showing = true; + } + + TestBed.configureTestingModule( + {declarations: [EmbeddedViewParent, OnPushComp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(EmbeddedViewParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.cdr.markForCheck(); + // markForCheck should not trigger change detection on its own. + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - two'); + + fixture.componentInstance.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - two'); + + fixture.componentInstance.comp.cdr.markForCheck(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two - two'); + }); + + // TODO(kara): add test for dynamic views once bug fix is in + }); + + describe('checkNoChanges', () => { + let comp: NoChangesComp; + + @Component({selector: 'no-changes-comp', template: '{{ value }}'}) + class NoChangesComp { + value = 1; + doCheckCount = 0; + contentCheckCount = 0; + viewCheckCount = 0; + + ngDoCheck() { this.doCheckCount++; } + + ngAfterContentChecked() { this.contentCheckCount++; } + + ngAfterViewChecked() { this.viewCheckCount++; } + + constructor(public cdr: ChangeDetectorRef) { comp = this; } + } + + @Component({template: '{{ value }} - '}) + class AppComp { + value = 1; + + constructor(public cdr: ChangeDetectorRef) {} + } + + // Custom error handler that just rethrows all the errors from the + // view, rather than logging them out. Used to keep our logs clean. + class RethrowErrorHandler extends ErrorHandler { + handleError(error: any) { throw error; } + } + + it('should throw if bindings in current view have changed', () => { + TestBed.configureTestingModule({ + declarations: [NoChangesComp], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + const fixture = TestBed.createComponent(NoChangesComp); + + expect(() => { fixture.componentInstance.cdr.checkNoChanges(); }) + .toThrowError( + /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*1'/gi); + }); + + it('should throw if interpolations in current view have changed', () => { + TestBed.configureTestingModule({ + declarations: [AppComp, NoChangesComp], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + const fixture = TestBed.createComponent(AppComp); + + expect(() => fixture.componentInstance.cdr.checkNoChanges()) + .toThrowError( + /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*1'/gi); + }); + + it('should throw if bindings in embedded view have changed', () => { + @Component({template: '{{ showing }}'}) + class EmbeddedViewApp { + showing = true; + constructor(public cdr: ChangeDetectorRef) {} + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewApp], + imports: [CommonModule], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + const fixture = TestBed.createComponent(EmbeddedViewApp); + + expect(() => fixture.componentInstance.cdr.checkNoChanges()) + .toThrowError( + /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*true'/gi); + }); + + it('should NOT call lifecycle hooks', () => { + TestBed.configureTestingModule({ + declarations: [AppComp, NoChangesComp], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + + const fixture = TestBed.createComponent(AppComp); + fixture.detectChanges(); + + expect(comp.doCheckCount).toEqual(1); + expect(comp.contentCheckCount).toEqual(1); + expect(comp.viewCheckCount).toEqual(1); + + comp.value = 2; + expect(() => fixture.componentInstance.cdr.checkNoChanges()).toThrow(); + expect(comp.doCheckCount).toEqual(1); + expect(comp.contentCheckCount).toEqual(1); + expect(comp.viewCheckCount).toEqual(1); + }); + + }); + + }); + +}); diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index c5e38d76d5..52629da45b 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -6,18 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {EmbeddedViewRef, TemplateRef, ViewContainerRef} from '@angular/core'; import {withBody} from '@angular/private/testing'; -import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core'; +import {ChangeDetectionStrategy, DoCheck} from '../../src/core'; import {whenRendered} from '../../src/render3/component'; -import {LifecycleHooksFeature, getRenderedText, ɵɵNgOnChangesFeature, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵgetCurrentView, ɵɵtemplateRefExtractor} from '../../src/render3/index'; -import {detectChanges, markDirty, tick, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵlistener, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; +import {LifecycleHooksFeature, getRenderedText, ɵɵdefineComponent, ɵɵgetCurrentView} from '../../src/render3/index'; +import {detectChanges, markDirty, tick, ɵɵbind, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵlistener, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {RElement, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer'; +import {Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer'; import {FLAGS, LViewFlags} from '../../src/render3/interfaces/view'; -import {ComponentFixture, containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util'; +import {containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util'; describe('change detection', () => { describe('markDirty, detectChanges, whenRendered, getRenderedText', () => { @@ -129,175 +128,6 @@ describe('change detection', () => { }); } - class MyApp { - name: string = 'Nancy'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 1, - vars: 1, - /** */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'name', ɵɵbind(ctx.name)); - } - }, - directives: () => [MyComponent] - }); - } - - it('should check OnPush components on initialization', () => { - const myApp = renderComponent(MyApp); - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - }); - - it('should call doCheck even when OnPush components are not dirty', () => { - const myApp = renderComponent(MyApp); - - tick(myApp); - expect(comp.doCheckCount).toEqual(2); - - tick(myApp); - expect(comp.doCheckCount).toEqual(3); - }); - - it('should skip OnPush components in update mode when they are not dirty', () => { - const myApp = renderComponent(MyApp); - - tick(myApp); - // doCheckCount is 2, but 1 should be rendered since it has not been marked dirty. - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - - tick(myApp); - // doCheckCount is 3, but 1 should be rendered since it has not been marked dirty. - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - }); - - it('should check OnPush components in update mode when inputs change', () => { - const myApp = renderComponent(MyApp); - - myApp.name = 'Bess'; - tick(myApp); - expect(comp.doCheckCount).toEqual(2); - // View should update, as changed input marks view dirty - expect(getRenderedText(myApp)).toEqual('2 - Bess'); - - myApp.name = 'George'; - tick(myApp); - // View should update, as changed input marks view dirty - expect(comp.doCheckCount).toEqual(3); - expect(getRenderedText(myApp)).toEqual('3 - George'); - - tick(myApp); - expect(comp.doCheckCount).toEqual(4); - // View should not be updated to "4", as inputs have not changed. - expect(getRenderedText(myApp)).toEqual('3 - George'); - }); - - it('should check OnPush components in update mode when component events occur', () => { - const myApp = renderComponent(MyApp); - expect(comp.doCheckCount).toEqual(1); - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - - const button = containerEl.querySelector('button') !; - button.click(); - requestAnimationFrame.flush(); - // No ticks should have been scheduled. - expect(comp.doCheckCount).toEqual(1); - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - - tick(myApp); - // Because the onPush comp should be dirty, it should update once CD runs - expect(comp.doCheckCount).toEqual(2); - expect(getRenderedText(myApp)).toEqual('2 - Nancy'); - }); - - it('should not check OnPush components in update mode when parent events occur', () => { - function noop() {} - - const ButtonParent = createComponent('button-parent', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - ɵɵelementStart(1, 'button', ['id', 'parent']); - { ɵɵlistener('click', () => noop()); } - ɵɵelementEnd(); - } - }, 2, 0, [MyComponent]); - - const buttonParent = renderComponent(ButtonParent); - expect(getRenderedText(buttonParent)).toEqual('1 - Nancy'); - - const button = containerEl.querySelector('button#parent') !; - (button as HTMLButtonElement).click(); - tick(buttonParent); - // The comp should still be clean. So doCheck will run, but the view should display 1. - expect(comp.doCheckCount).toEqual(2); - expect(getRenderedText(buttonParent)).toEqual('1 - Nancy'); - }); - - it('should check parent OnPush components in update mode when child events occur', () => { - let parent: ButtonParent; - - class ButtonParent implements DoCheck { - doCheckCount = 0; - ngDoCheck(): void { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: ButtonParent, - selectors: [['button-parent']], - factory: () => parent = new ButtonParent(), - consts: 2, - vars: 1, - /** {{ doCheckCount }} - */ - template: (rf: RenderFlags, ctx: ButtonParent) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.doCheckCount, ' - ')); - } - }, - directives: () => [MyComponent], - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'button-parent'); - } - }, 1, 0, [ButtonParent]); - - const myButtonApp = renderComponent(MyButtonApp); - expect(parent !.doCheckCount).toEqual(1); - expect(comp !.doCheckCount).toEqual(1); - expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy'); - - tick(myButtonApp); - expect(parent !.doCheckCount).toEqual(2); - // parent isn't checked, so child doCheck won't run - expect(comp !.doCheckCount).toEqual(1); - expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy'); - - const button = containerEl.querySelector('button'); - button !.click(); - requestAnimationFrame.flush(); - // No ticks should have been scheduled. - expect(parent !.doCheckCount).toEqual(2); - expect(comp !.doCheckCount).toEqual(1); - - tick(myButtonApp); - expect(parent !.doCheckCount).toEqual(3); - expect(comp !.doCheckCount).toEqual(2); - expect(getRenderedText(myButtonApp)).toEqual('3 - 2 - Nancy'); - }); - describe('Manual mode', () => { class ManualComponent implements DoCheck { /* @Input() */ @@ -420,12 +250,11 @@ describe('change detection', () => { }); } - const MyButtonApp = - createComponent('my-button-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'button-parent'); - } - }, 1, 0, [ButtonParent]); + const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags) { + if (rf & RenderFlags.Create) { + ɵɵelement(0, 'button-parent'); + } + }, 1, 0, [ButtonParent]); const myButtonApp = renderComponent(MyButtonApp); expect(parent !.doCheckCount).toEqual(1); @@ -462,997 +291,11 @@ describe('change detection', () => { }); }); - describe('ChangeDetectorRef', () => { - - describe('detectChanges()', () => { - let myComp: MyComp; - let dir: Dir; - - class MyComp { - doCheckCount = 0; - name = 'Nancy'; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - selectors: [['my-comp']], - factory: () => myComp = new MyComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ name }} */ - template: (rf: RenderFlags, ctx: MyComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.name)); - } - }, - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - class ParentComp { - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - factory: () => new ParentComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 2, - vars: 1, - /** - * {{ doCheckCount}} - - * - */ - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.doCheckCount, ' - ')); - } - }, - directives: () => [MyComp] - }); - } - - class Dir { - constructor(public cdr: ChangeDetectorRef) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Dir, - selectors: [['', 'dir', '']], - factory: () => dir = new Dir(ɵɵdirectiveInject(ChangeDetectorRef as any)) - }); - } - - - it('should check the component view when called by component (even when OnPush && clean)', - () => { - const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(comp)).toEqual('Nancy'); - - comp.name = 'Bess'; // as this is not an Input, the component stays clean - comp.cdr.detectChanges(); - expect(getRenderedText(comp)).toEqual('Bess'); - }); - - it('should NOT call component doCheck when called by a component', () => { - const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(comp.doCheckCount).toEqual(1); - - // NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you - // may see doCheck called in some cases bc of the extra CD run triggered by zone.js. - // It's important not to call doCheck to allow calls to detectChanges in that hook. - comp.cdr.detectChanges(); - expect(comp.doCheckCount).toEqual(1); - }); - - it('should NOT check the component parent when called by a child component', () => { - const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); - - parentComp.doCheckCount = 100; - myComp.cdr.detectChanges(); - expect(parentComp.doCheckCount).toEqual(100); - expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); - }); - - it('should check component children when called by component if dirty or check-always', - () => { - const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(parentComp.doCheckCount).toEqual(1); - - myComp.name = 'Bess'; - parentComp.cdr.detectChanges(); - expect(parentComp.doCheckCount).toEqual(1); - expect(myComp.doCheckCount).toEqual(2); - // OnPush child is not dirty, so its change isn't rendered. - expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); - }); - - it('should not group detectChanges calls (call every time)', () => { - const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(myComp.doCheckCount).toEqual(1); - - parentComp.cdr.detectChanges(); - parentComp.cdr.detectChanges(); - expect(myComp.doCheckCount).toEqual(3); - }); - - it('should check component view when called by directive on component node', () => { - /** */ - const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp', ['dir', '']); - } - }, 1, 0, [MyComp, Dir]); - - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('Nancy'); - - myComp.name = 'George'; - dir !.cdr.detectChanges(); - expect(getRenderedText(app)).toEqual('George'); - }); - - it('should check host component when called by directive on element node', () => { - /** - * {{ name }} - *
    - */ - const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'div', ['dir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(ctx.value)); - } - }, 2, 1, [Dir]); - - const app = renderComponent(MyApp); - app.value = 'Frank'; - tick(app); - expect(getRenderedText(app)).toEqual('Frank'); - - app.value = 'Joe'; - dir !.cdr.detectChanges(); - expect(getRenderedText(app)).toEqual('Joe'); - }); - - it('should check the host component when called from EmbeddedViewRef', () => { - class MyApp { - showing = true; - name = 'Amelia'; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 2, - vars: 1, - /** - * {{ name}} - * % if (showing) { - *
    - * % } - */ - template: function(rf: RenderFlags, ctx: MyApp) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵcontainer(1); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.name)); - ɵɵcontainerRefreshStart(1); - { - if (ctx.showing) { - let rf0 = ɵɵembeddedViewStart(0, 1, 0); - if (rf0 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - } - ɵɵembeddedViewEnd(); - } - ɵɵcontainerRefreshEnd(); - } - }, - directives: [Dir] - }); - } - - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('Amelia'); - - app.name = 'Emerson'; - dir !.cdr.detectChanges(); - expect(getRenderedText(app)).toEqual('Emerson'); - }); - - it('should support call in ngOnInit', () => { - class DetectChangesComp { - value = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngOnInit() { - this.value++; - this.cdr.detectChanges(); - } - - static ngComponentDef = ɵɵdefineComponent({ - type: DetectChangesComp, - selectors: [['detect-changes-comp']], - factory: () => new DetectChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: DetectChangesComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(comp)).toEqual('1'); - }); - - - ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => { - it(`should not go infinite loop when recursively called from children's ng${hook}`, () => { - class ChildComp { - // @Input - inp = ''; - - count = 0; - constructor(public parentComp: ParentComp) {} - - ngOnInit() { this.check('OnInit'); } - ngAfterContentInit() { this.check('AfterContentInit'); } - ngAfterViewInit() { this.check('AfterViewInit'); } - ngOnChanges() { this.check('OnChanges'); } - - check(h: string) { - if (h === hook) { - this.count++; - if (this.count > 1) throw new Error(`ng${hook} should be called only once!`); - this.parentComp.triggerChangeDetection(); - } - } - - static ngComponentDef = ɵɵdefineComponent({ - type: ChildComp, - selectors: [['child-comp']], - factory: () => new ChildComp(ɵɵdirectiveInject(ParentComp as any)), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: ChildComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'foo'); - } - }, - inputs: {inp: 'inp'}, - features: [ɵɵNgOnChangesFeature] - }); - } - - class ParentComp { - constructor(public cdr: ChangeDetectorRef) {} - - triggerChangeDetection() { this.cdr.detectChanges(); } - - static ngComponentDef = ɵɵdefineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - factory: () => new ParentComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'child-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'inp', ɵɵbind(true)); - } - }, - directives: [ChildComp] - }); - } - - expect(() => renderComponent(ParentComp)).not.toThrow(); - }); - }); - - it('should support call in ngDoCheck', () => { - class DetectChangesComp { - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { - this.doCheckCount++; - this.cdr.detectChanges(); - } - - static ngComponentDef = ɵɵdefineComponent({ - type: DetectChangesComp, - selectors: [['detect-changes-comp']], - factory: () => new DetectChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ doCheckCount }} */ - template: (rf: RenderFlags, ctx: DetectChangesComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.doCheckCount)); - } - } - }); - } - - const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(comp)).toEqual('1'); - }); - - describe('dynamic views', () => { - let structuralComp: StructuralComp|null = null; - - beforeEach(() => structuralComp = null); - - class StructuralComp { - tmp !: TemplateRef; - value = 'one'; - - constructor(public vcr: ViewContainerRef) {} - - create() { return this.vcr.createEmbeddedView(this.tmp, this); } - - static ngComponentDef = ɵɵdefineComponent({ - type: StructuralComp, - selectors: [['structural-comp']], - factory: () => structuralComp = - new StructuralComp(ɵɵdirectiveInject(ViewContainerRef as any)), - inputs: {tmp: 'tmp'}, - consts: 1, - vars: 1, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - it('should support ViewRef.detectChanges()', () => { - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - - /** - * {{ value }} - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, FooTemplate, 1, 1, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'structural-comp'); - } - if (rf & RenderFlags.Update) { - const foo = ɵɵreference(1) as any; - ɵɵelementProperty(2, 'tmp', ɵɵbind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(fixture.html).toEqual('one'); - - const viewRef: EmbeddedViewRef = structuralComp !.create(); - fixture.update(); - expect(fixture.html).toEqual('oneone'); - - // check embedded view update - structuralComp !.value = 'two'; - viewRef.detectChanges(); - expect(fixture.html).toEqual('onetwo'); - - // check root view update - structuralComp !.value = 'three'; - fixture.update(); - expect(fixture.html).toEqual('threethree'); - }); - - it('should support ViewRef.detectChanges() directly after creation', () => { - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Template text'); - } - } - - /** - * Template text - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, FooTemplate, 1, 0, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'structural-comp'); - } - if (rf & RenderFlags.Update) { - const foo = ɵɵreference(1) as any; - ɵɵelementProperty(2, 'tmp', ɵɵbind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(fixture.html).toEqual('one'); - - const viewRef: EmbeddedViewRef = structuralComp !.create(); - viewRef.detectChanges(); - expect(fixture.html).toEqual('oneTemplate text'); - }); - - }); - - }); - - describe('attach/detach', () => { - let comp: DetachedComp; - - class MyApp { - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 0, - /** */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'detached-comp'); - } - }, - directives: () => [DetachedComp] - }); - } - - class DetachedComp { - value = 'one'; - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: DetachedComp, - selectors: [['detached-comp']], - factory: () => comp = new DetachedComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: DetachedComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - it('should not check detached components', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - - comp.value = 'two'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - }); - - it('should check re-attached components', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - comp.value = 'two'; - - comp.cdr.reattach(); - tick(app); - expect(getRenderedText(app)).toEqual('two'); - }); - - it('should call lifecycle hooks on detached components', () => { - const app = renderComponent(MyApp); - expect(comp.doCheckCount).toEqual(1); - - comp.cdr.detach(); - - tick(app); - expect(comp.doCheckCount).toEqual(2); - }); - - it('should check detached component when detectChanges is called', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - - comp.value = 'two'; - detectChanges(comp); - expect(getRenderedText(app)).toEqual('two'); - }); - - it('should not check detached component when markDirty is called', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - - comp.value = 'two'; - markDirty(comp); - requestAnimationFrame.flush(); - - expect(getRenderedText(app)).toEqual('one'); - }); - - it('should detach any child components when parent is detached', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - app.cdr.detach(); - - comp.value = 'two'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - - app.cdr.reattach(); - - tick(app); - expect(getRenderedText(app)).toEqual('two'); - }); - - it('should detach OnPush components properly', () => { - let onPushComp: OnPushComp; - - class OnPushComp { - /** @Input() */ - // TODO(issue/24571): remove '!'. - value !: string; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: OnPushComp, - selectors: [['on-push-comp']], - factory: () => onPushComp = new OnPushComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - }, - changeDetection: ChangeDetectionStrategy.OnPush, - inputs: {value: 'value'} - }); - } - - /** */ - const OnPushApp = createComponent('on-push-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'on-push-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'value', ɵɵbind(ctx.value)); - } - }, 1, 1, [OnPushComp]); - - const app = renderComponent(OnPushApp); - app.value = 'one'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - - onPushComp !.cdr.detach(); - - app.value = 'two'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - - onPushComp !.cdr.reattach(); - - tick(app); - expect(getRenderedText(app)).toEqual('two'); - }); - - }); - - describe('markForCheck()', () => { - let comp: OnPushComp; - - class OnPushComp { - value = 'one'; - - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: OnPushComp, - selectors: [['on-push-comp']], - factory: () => comp = new OnPushComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: OnPushComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - }, - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - class OnPushParent { - value = 'one'; - - static ngComponentDef = ɵɵdefineComponent({ - type: OnPushParent, - selectors: [['on-push-parent']], - factory: () => new OnPushParent(), - consts: 2, - vars: 1, - /** - * {{ value }} - - * - */ - template: (rf: RenderFlags, ctx: OnPushParent) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'on-push-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - ')); - } - }, - directives: () => [OnPushComp], - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - it('should ensure OnPush components are checked', () => { - const fixture = new ComponentFixture(OnPushParent); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.cdr.markForCheck(); - - // Change detection should not have run yet, since markForCheck - // does not itself schedule change detection. - expect(fixture.hostElement.textContent).toEqual('one - one'); - - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - two'); - }); - - it('should never schedule change detection on its own', () => { - const fixture = new ComponentFixture(OnPushParent); - expect(comp.doCheckCount).toEqual(1); - - comp.cdr.markForCheck(); - comp.cdr.markForCheck(); - requestAnimationFrame.flush(); - - expect(comp.doCheckCount).toEqual(1); - }); - - it('should ensure ancestor OnPush components are checked', () => { - const fixture = new ComponentFixture(OnPushParent); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - fixture.component.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.cdr.markForCheck(); - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('two - one'); - - }); - - it('should ensure OnPush components in embedded views are checked', () => { - class EmbeddedViewParent { - value = 'one'; - showing = true; - - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedViewParent, - selectors: [['embedded-view-parent']], - factory: () => new EmbeddedViewParent(), - consts: 2, - vars: 1, - /** - * {{ value }} - - * % if (ctx.showing) { - * - * % } - */ - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵcontainer(1); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - ')); - ɵɵcontainerRefreshStart(1); - { - if (ctx.showing) { - let rf0 = ɵɵembeddedViewStart(0, 1, 0); - if (rf0 & RenderFlags.Create) { - ɵɵelement(0, 'on-push-comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - directives: () => [OnPushComp], - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - const fixture = new ComponentFixture(EmbeddedViewParent); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.cdr.markForCheck(); - // markForCheck should not trigger change detection on its own. - expect(fixture.hostElement.textContent).toEqual('one - one'); - - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - two'); - - fixture.component.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - two'); - - comp.cdr.markForCheck(); - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('two - two'); - }); - - // TODO(kara): add test for dynamic views once bug fix is in - }); - - describe('checkNoChanges', () => { - let comp: NoChangesComp; - - class NoChangesComp { - value = 1; - doCheckCount = 0; - contentCheckCount = 0; - viewCheckCount = 0; - - ngDoCheck() { this.doCheckCount++; } - - ngAfterContentChecked() { this.contentCheckCount++; } - - ngAfterViewChecked() { this.viewCheckCount++; } - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: NoChangesComp, - selectors: [['no-changes-comp']], - factory: () => comp = new NoChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: NoChangesComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - class AppComp { - value = 1; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: AppComp, - selectors: [['app-comp']], - factory: () => new AppComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 2, - vars: 1, - /** - * {{ value }} - - * - */ - template: (rf: RenderFlags, ctx: AppComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'no-changes-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - ')); - } - }, - directives: () => [NoChangesComp] - }); - } - - it('should throw if bindings in current view have changed', () => { - const comp = renderComponent(NoChangesComp, {hostFeatures: [LifecycleHooksFeature]}); - - expect(() => comp.cdr.checkNoChanges()).not.toThrow(); - - comp.value = 2; - expect(() => comp.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should throw if interpolations in current view have changed', () => { - const app = renderComponent(AppComp); - - expect(() => app.cdr.checkNoChanges()).not.toThrow(); - - app.value = 2; - expect(() => app.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should throw if bindings in children of current view have changed', () => { - const app = renderComponent(AppComp); - - expect(() => app.cdr.checkNoChanges()).not.toThrow(); - - comp.value = 2; - expect(() => app.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should throw if bindings in embedded view have changed', () => { - class EmbeddedViewApp { - value = 1; - showing = true; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedViewApp, - selectors: [['embedded-view-app']], - factory: () => new EmbeddedViewApp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 0, - /** - * % if (showing) { - * {{ value }} - * %} - */ - template: (rf: RenderFlags, ctx: EmbeddedViewApp) => { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.showing) { - let rf0 = ɵɵembeddedViewStart(0, 1, 1); - if (rf0 & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf0 & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - }); - } - - const app = renderComponent(EmbeddedViewApp); - - expect(() => app.cdr.checkNoChanges()).not.toThrow(); - - app.value = 2; - expect(() => app.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should NOT call lifecycle hooks', () => { - const app = renderComponent(AppComp); - expect(comp.doCheckCount).toEqual(1); - expect(comp.contentCheckCount).toEqual(1); - expect(comp.viewCheckCount).toEqual(1); - - comp.value = 2; - expect(() => app.cdr.checkNoChanges()).toThrow(); - expect(comp.doCheckCount).toEqual(1); - expect(comp.contentCheckCount).toEqual(1); - expect(comp.viewCheckCount).toEqual(1); - }); - - it('should NOT throw if bindings in ancestors of current view have changed', () => { - const app = renderComponent(AppComp); - - app.value = 2; - expect(() => comp.cdr.checkNoChanges()).not.toThrow(); - }); - - }); - - }); - it('should call begin and end when the renderer factory implements them', () => { const log: string[] = []; const testRendererFactory: RendererFactory3 = { - createRenderer: (hostElement: RElement | null, rendererType: RendererType2 | null): - Renderer3 => { return document; }, + createRenderer: (): Renderer3 => { return document; }, begin: () => log.push('begin'), end: () => log.push('end'), }; From cc2f175617dae7a7cb95a61ae00033ba67a2fbbd Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sat, 11 May 2019 08:50:12 -0400 Subject: [PATCH 018/275] test(ivy): move discovery_utils tests to acceptance (#30416) Rewrites the discovery util tests to use `TestBed`. PR Close #30416 --- .../discover_utils_spec.ts} | 359 +++++------------- 1 file changed, 95 insertions(+), 264 deletions(-) rename packages/core/test/{render3/discovery_utils_spec.ts => acceptance/discover_utils_spec.ts} (51%) diff --git a/packages/core/test/render3/discovery_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts similarity index 51% rename from packages/core/test/render3/discovery_utils_spec.ts rename to packages/core/test/acceptance/discover_utils_spec.ts index ac6acb306d..d55eb3d91d 100644 --- a/packages/core/test/render3/discovery_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -5,16 +5,15 @@ * 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 {StaticInjector} from '../../src/di/injector'; -import {createInjector} from '../../src/di/r3_injector'; -import {AttributeMarker, RenderFlags, getHostElement, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵi18n, ɵɵi18nApply, ɵɵi18nExp, ɵɵselect} from '../../src/render3/index'; -import {markDirty, ɵɵbind, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵlistener, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, InjectionToken, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {onlyInIvy} from '@angular/private/testing'; + +import {getHostElement, markDirty} from '../../src/render3/index'; import {getComponent, getContext, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getRootComponents, getViewComponent, loadLContext} from '../../src/render3/util/discovery_utils'; -import {NgIf} from './common_with_def'; -import {ComponentFixture} from './render_util'; - -describe('discovery utils', () => { +onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { let fixture: ComponentFixture; let myApp: MyApp; let dirA: DirectiveA[]; @@ -29,115 +28,46 @@ describe('discovery utils', () => { log = []; dirA = []; childComponent = []; - fixture = new ComponentFixture( - MyApp, {injector: createInjector(null, null, [{provide: String, useValue: 'Module'}])}); - child = fixture.hostElement.querySelectorAll('child'); - span = fixture.hostElement.querySelectorAll('span'); - div = fixture.hostElement.querySelectorAll('div'); - p = fixture.hostElement.querySelectorAll('p'); + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [MyApp, DirectiveA, Child], + providers: [{provide: String, useValue: 'Module'}] + }); + fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + child = fixture.nativeElement.querySelectorAll('child'); + span = fixture.nativeElement.querySelectorAll('span'); + div = fixture.nativeElement.querySelectorAll('div'); + p = fixture.nativeElement.querySelectorAll('p'); }); - /** - * For all tests assume this set up - * - * ``` - * - * <#VIEW> - * {{text}} - *
    - * - * <#VIEW> - *

    - * - *
    - * - * <#VIEW> - *

    - * - *
    - * - * <#VIEW> - *

    - * - *
    - * ICU expression - * - *
    - * ``` - */ + @Component( + {selector: 'child', template: '

    ', providers: [{provide: String, useValue: 'Child'}]}) class Child { constructor() { childComponent.push(this); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Child) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'p'); - } - }, - features: [ɵɵProvidersFeature([{provide: String, useValue: 'Child'}])] - }); } + @Directive({selector: '[dirA]', exportAs: 'dirA'}) class DirectiveA { constructor() { dirA.push(this); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['', 'dirA', '']], - exportAs: ['dirA'], - factory: () => new DirectiveA(), - }); } - const MSG_DIV = `{�0�, select, - other {ICU expression} - }`; - + @Component({ + selector: 'my-app', + template: ` + {{text}} +
    + + + +

    + ` + }) class MyApp { text: string = 'INIT'; constructor() { myApp = this; } - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 13, - vars: 1, - directives: [Child, DirectiveA, NgIf], - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵlistener('click', $event => log.push($event)); - ɵɵtext(1); - ɵɵelementEnd(); - ɵɵelement(2, 'div', ['dirA', ''], ['div', '', 'foo', 'dirA']); - ɵɵelement(5, 'child'); - ɵɵelement(6, 'child', ['dirA', ''], ['child', '']); - ɵɵtemplate(8, function(rf: RenderFlags, ctx: never) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'child'); - } - }, 1, 0, 'child', ['dirA', AttributeMarker.Template, 'ngIf']); - ɵɵelementStart(9, 'i18n'); - ɵɵi18n(10, MSG_DIV); - ɵɵelementEnd(); - ɵɵelementContainerStart(11); - { ɵɵtext(12, 'content'); } - ɵɵelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(ctx.text)); - ɵɵelementProperty(8, 'ngIf', ɵɵbind(true)); - ɵɵi18nExp(ɵɵbind(ctx.text)); - ɵɵi18nApply(10); - } - } - }); + log(event: any) { log.push(event); } } describe('getComponent', () => { @@ -151,7 +81,7 @@ describe('discovery utils', () => { expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/); }); it('should return component from element', () => { - expect(getComponent(fixture.hostElement)).toEqual(myApp); + expect(getComponent(fixture.nativeElement)).toEqual(myApp); expect(getComponent(child[0])).toEqual(childComponent[0]); expect(getComponent(child[1])).toEqual(childComponent[1]); }); @@ -171,7 +101,7 @@ describe('discovery utils', () => { describe('getHostElement', () => { it('should return element on component', () => { - expect(getHostElement(myApp)).toEqual(fixture.hostElement); + expect(getHostElement(myApp)).toEqual(fixture.nativeElement); expect(getHostElement(childComponent[0])).toEqual(child[0]); expect(getHostElement(childComponent[1])).toEqual(child[1]); }); @@ -186,7 +116,7 @@ describe('discovery utils', () => { describe('getInjector', () => { it('should return node-injector from element', () => { - expect(getInjector(fixture.hostElement).get(String)).toEqual('Module'); + expect(getInjector(fixture.nativeElement).get(String)).toEqual('Module'); expect(getInjector(child[0]).get(String)).toEqual('Child'); expect(getInjector(p[0]).get(String)).toEqual('Child'); }); @@ -203,7 +133,7 @@ describe('discovery utils', () => { describe('getDirectives', () => { it('should return empty array if no directives', () => { - expect(getDirectives(fixture.hostElement)).toEqual([]); + expect(getDirectives(fixture.nativeElement)).toEqual([]); expect(getDirectives(span[0])).toEqual([]); expect(getDirectives(child[0])).toEqual([]); }); @@ -215,7 +145,7 @@ describe('discovery utils', () => { describe('getViewComponent', () => { it('should return null when called on root component', () => { - expect(getViewComponent(fixture.hostElement)).toEqual(null); + expect(getViewComponent(fixture.nativeElement)).toEqual(null); expect(getViewComponent(myApp)).toEqual(null); }); it('should return containing component of child component', () => { @@ -242,7 +172,7 @@ describe('discovery utils', () => { describe('getLocalRefs', () => { it('should retrieve empty map', () => { - expect(getLocalRefs(fixture.hostElement)).toEqual({}); + expect(getLocalRefs(fixture.nativeElement)).toEqual({}); expect(getLocalRefs(myApp)).toEqual({}); expect(getLocalRefs(span[0])).toEqual({}); expect(getLocalRefs(child[0])).toEqual({}); @@ -274,7 +204,7 @@ describe('discovery utils', () => { describe('getListeners', () => { it('should return no listeners', () => { - expect(getListeners(fixture.hostElement)).toEqual([]); + expect(getListeners(fixture.nativeElement)).toEqual([]); expect(getListeners(child[0])).toEqual([]); }); it('should return the listeners', () => { @@ -290,7 +220,7 @@ describe('discovery utils', () => { describe('getInjectionTokens', () => { it('should retrieve tokens', () => { - expect(getInjectionTokens(fixture.hostElement)).toEqual([MyApp]); + expect(getInjectionTokens(fixture.nativeElement)).toEqual([MyApp]); expect(getInjectionTokens(child[0])).toEqual([String, Child]); expect(getInjectionTokens(child[1])).toEqual([String, Child, DirectiveA]); }); @@ -301,7 +231,7 @@ describe('discovery utils', () => { expect(span[0].textContent).toEqual('INIT'); myApp.text = 'WORKS'; markDirty(myApp); - fixture.requestAnimationFrame.flush(); + fixture.detectChanges(); expect(span[0].textContent).toEqual('WORKS'); }); }); @@ -314,23 +244,15 @@ describe('discovery utils', () => { }); it('should work on templates', () => { - const templateComment = Array.from(fixture.hostElement.childNodes) + const templateComment = Array.from(fixture.nativeElement.childNodes) .find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE) !; const lContext = loadLContext(templateComment); expect(lContext).toBeDefined(); expect(lContext.native as any).toBe(templateComment); }); - it('should work on ICU expressions', () => { - const icuComment = Array.from(fixture.hostElement.querySelector('i18n') !.childNodes) - .find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE) !; - const lContext = loadLContext(icuComment); - expect(lContext).toBeDefined(); - expect(lContext.native as any).toBe(icuComment); - }); - it('should work on ng-container', () => { - const ngContainerComment = Array.from(fixture.hostElement.childNodes) + const ngContainerComment = Array.from(fixture.nativeElement.childNodes) .find( (node: ChildNode) => node.nodeType === Node.COMMENT_NODE && node.textContent === `ng-container`) !; @@ -341,50 +263,26 @@ describe('discovery utils', () => { }); }); -describe('discovery utils deprecated', () => { - +onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => { describe('getRootComponents()', () => { it('should return a list of the root components of the application from an element', () => { - let innerComp: InnerComp; + @Component({selector: 'inner-comp', template: '
    '}) class InnerComp { - static ngComponentDef = ɵɵdefineComponent({ - type: InnerComp, - selectors: [['inner-comp']], - factory: () => innerComp = new InnerComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: InnerComp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div'); - } - } - }); } + @Component({selector: 'comp', template: ''}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'inner-comp'); - } - }, - directives: [InnerComp] - }); } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp, InnerComp]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const hostElm = fixture.hostElement; + const hostElm = fixture.nativeElement; const innerElm = hostElm.querySelector('inner-comp') !; const divElm = hostElm.querySelector('div') !; - const component = fixture.component; + const component = fixture.componentInstance; expect(getRootComponents(hostElm) !).toEqual([component]); expect(getRootComponents(innerElm) !).toEqual([component]); @@ -394,65 +292,46 @@ describe('discovery utils deprecated', () => { describe('getDirectives()', () => { it('should return a list of the directives that are on the given element', () => { - let myDir1Instance: MyDir1|null = null; - let myDir2Instance: MyDir2|null = null; - let myDir3Instance: MyDir2|null = null; - + @Directive({selector: '[my-dir-1]'}) class MyDir1 { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir1, - selectors: [['', 'my-dir-1', '']], - factory: () => myDir1Instance = new MyDir1() - }); } + @Directive({selector: '[my-dir-2]'}) class MyDir2 { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir2, - selectors: [['', 'my-dir-2', '']], - factory: () => myDir2Instance = new MyDir2() - }); } + @Directive({selector: '[my-dir-3]'}) class MyDir3 { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir3, - selectors: [['', 'my-dir-3', '']], - factory: () => myDir3Instance = new MyDir2() - }); } + @Component({ + selector: 'comp', + template: ` +
    +
    + ` + }) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['my-dir-1', '', 'my-dir-2', '']); - ɵɵelement(1, 'div', ['my-dir-3']); - } - }, - directives: [MyDir1, MyDir2, MyDir3] - }); + @ViewChild(MyDir1) myDir1Instance !: MyDir1; + @ViewChild(MyDir2) myDir2Instance !: MyDir2; + @ViewChild(MyDir3) myDir3Instance !: MyDir3; } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp, MyDir1, MyDir2, MyDir3]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const hostElm = fixture.hostElement; + const hostElm = fixture.nativeElement; const elements = hostElm.querySelectorAll('div'); const elm1 = elements[0]; const elm1Dirs = getDirectives(elm1); - expect(elm1Dirs).toContain(myDir1Instance !); - expect(elm1Dirs).toContain(myDir2Instance !); + expect(elm1Dirs).toContain(fixture.componentInstance.myDir1Instance !); + expect(elm1Dirs).toContain(fixture.componentInstance.myDir2Instance !); const elm2 = elements[1]; const elm2Dirs = getDirectives(elm2); - expect(elm2Dirs).toContain(myDir3Instance !); + expect(elm2Dirs).toContain(fixture.componentInstance.myDir3Instance !); }); }); @@ -460,81 +339,48 @@ describe('discovery utils deprecated', () => { it('should return an injector that can return directive instances', () => { + @Component({template: ''}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => {} - }); } - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const nodeInjector = getInjector(fixture.hostElement); + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + const nodeInjector = getInjector(fixture.nativeElement); expect(nodeInjector.get(Comp)).toEqual(jasmine.any(Comp)); }); it('should return an injector that falls-back to a module injector', () => { - + @Component({template: ''}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => {} - }); } class TestToken {} + const token = new InjectionToken('test token'); - const staticInjector = new StaticInjector([{provide: TestToken, useValue: new TestToken()}]); - const fixture = new ComponentFixture(Comp, {injector: staticInjector}); - fixture.update(); - - const nodeInjector = getInjector(fixture.hostElement); - expect(nodeInjector.get(TestToken)).toEqual(jasmine.any(TestToken)); + TestBed.configureTestingModule( + {declarations: [Comp], providers: [{provide: token, useValue: new TestToken()}]}); + const fixture = TestBed.createComponent(Comp); + const nodeInjector = getInjector(fixture.nativeElement); + expect(nodeInjector.get(token)).toEqual(jasmine.any(TestToken)); }); }); describe('getLocalRefs', () => { it('should return a map of local refs for an element', () => { + @Directive({selector: '[myDir]', exportAs: 'myDir'}) class MyDir { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir, - selectors: [['', 'myDir', '']], - exportAs: ['myDir'], - factory: () => new MyDir() - }); } + @Component({template: '
    '}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - //
    - ɵɵelement(0, 'div', ['myDir'], ['elRef', '', 'dirRef', 'myDir']); - } - }, - directives: [MyDir] - }); } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp, MyDir]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const divEl = fixture.hostElement.querySelector('div') !; + const divEl = fixture.nativeElement.querySelector('div') !; const localRefs = getLocalRefs(divEl); expect(localRefs.elRef.tagName.toLowerCase()).toBe('div'); @@ -542,31 +388,16 @@ describe('discovery utils deprecated', () => { }); it('should return a map of local refs for an element with styling context', () => { + @Component({template: '
    '}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - //
    - ɵɵelementStart(0, 'div', null, ['elRef', '']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstylingApply(); - } - } - }); + color = 'red'; } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const divEl = fixture.hostElement.querySelector('div') !; + const divEl = fixture.nativeElement.querySelector('div') !; const localRefs = getLocalRefs(divEl); expect(localRefs.elRef.tagName.toLowerCase()).toBe('div'); From 34e0d621fd438f835d6a0046a3ec0d3982c2c1aa Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Tue, 14 May 2019 03:10:12 +0530 Subject: [PATCH 019/275] build(docs-infra): update http-server to 0.11.1 (#30401) Fixes #30363 PR Close #30401 --- aio/package.json | 2 +- aio/tools/examples/shared/package.json | 2 +- aio/tools/examples/shared/yarn.lock | 65 ++++++++++++++------------ aio/yarn.lock | 53 +++++++++------------ 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/aio/package.json b/aio/package.json index d09bd5c267..0428297960 100644 --- a/aio/package.json +++ b/aio/package.json @@ -123,7 +123,7 @@ "hast-util-is-element": "^1.0.0", "hast-util-to-string": "^1.0.0", "html": "^1.0.0", - "http-server": "^0.9.0", + "http-server": "^0.11.1", "ignore": "^3.3.3", "image-size": "^0.5.1", "jasmine": "^2.6.0", diff --git a/aio/tools/examples/shared/package.json b/aio/tools/examples/shared/package.json index 198677f158..c439800643 100644 --- a/aio/tools/examples/shared/package.json +++ b/aio/tools/examples/shared/package.json @@ -59,7 +59,7 @@ "@types/node": "~8.9.4", "canonical-path": "1.0.0", "concurrently": "^3.0.0", - "http-server": "^0.9.0", + "http-server": "^0.11.1", "jasmine-core": "~2.99.1", "jasmine-marbles": "^0.4.0", "jasmine-spec-reporter": "~4.2.1", diff --git a/aio/tools/examples/shared/yarn.lock b/aio/tools/examples/shared/yarn.lock index 3540e04352..d74a31dac5 100644 --- a/aio/tools/examples/shared/yarn.lock +++ b/aio/tools/examples/shared/yarn.lock @@ -1006,10 +1006,6 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.0.tgz#ac3613b1da9bed1b47510bb4651b8931e47146c7" - async@1.5.2, async@1.x, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -2480,14 +2476,15 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -ecstatic@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-1.4.1.tgz#32cb7b6fa2e290d58668674d115e8f0c3d567d6a" +ecstatic@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.1.tgz#b15b5b036c2233defc78d7bacbd8765226c95577" + integrity sha512-/rrctvxZ78HMI/tPIsqdvFKHHscxR3IJuKrZI2ZoUgkt2SiufyLFBmcco+aqQBIu6P1qBsUNG3drAAGLx80vTQ== dependencies: - he "^0.5.0" - mime "^1.2.11" + he "^1.1.1" + mime "^1.6.0" minimist "^1.1.0" - url-join "^1.0.0" + url-join "^2.0.5" ee-first@1.1.1: version "1.1.1" @@ -3624,9 +3621,10 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" +he@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== hmac-drbg@^1.0.0: version "1.0.1" @@ -3726,17 +3724,18 @@ http-proxy@^1.13.0, http-proxy@^1.16.2, http-proxy@^1.8.1: eventemitter3 "1.x.x" requires-port "1.x.x" -http-server@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.9.0.tgz#8f1b06bdc733618d4dc42831c7ba1aff4e06001a" +http-server@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b" + integrity sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w== dependencies: colors "1.0.3" corser "~2.0.0" - ecstatic "^1.4.0" + ecstatic "^3.0.0" http-proxy "^1.8.1" opener "~1.4.0" optimist "0.6.x" - portfinder "0.4.x" + portfinder "^1.0.13" union "~0.4.3" http-signature@~1.1.0: @@ -4991,7 +4990,7 @@ mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" -mime@1.4.1, mime@^1.2.11, mime@^1.4.1: +mime@1.4.1, mime@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" @@ -4999,6 +4998,11 @@ mime@1.4.1, mime@^1.2.11, mime@^1.4.1: version "2.0.3" resolved "https://registry.yarnpkg.com/mime/-/mime-2.0.3.tgz#4353337854747c48ea498330dc034f9f4bbbcc0b" +mime@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" @@ -5965,13 +5969,6 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -portfinder@0.4.x: - version "0.4.0" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-0.4.0.tgz#a3ffadffafe4fb98e0601a85eda27c27ce84ca1e" - dependencies: - async "0.9.0" - mkdirp "0.5.x" - portfinder@1.0.17: version "1.0.17" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a" @@ -5980,6 +5977,15 @@ portfinder@1.0.17: debug "^2.2.0" mkdirp "0.5.x" +portfinder@^1.0.13: + version "1.0.20" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" + integrity sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw== + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + portfinder@^1.0.9: version "1.0.13" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" @@ -8071,9 +8077,10 @@ urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" -url-join@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" +url-join@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" + integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= url-parse@^1.4.3: version "1.4.4" diff --git a/aio/yarn.lock b/aio/yarn.lock index c98c1b7021..a69b3ff956 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -1078,10 +1078,6 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.0.tgz#ac3613b1da9bed1b47510bb4651b8931e47146c7" - async@2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" @@ -3094,14 +3090,15 @@ ecdsa-sig-formatter@1.0.9: base64url "^2.0.0" safe-buffer "^5.0.1" -ecstatic@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-1.4.1.tgz#32cb7b6fa2e290d58668674d115e8f0c3d567d6a" +ecstatic@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.1.tgz#b15b5b036c2233defc78d7bacbd8765226c95577" + integrity sha512-/rrctvxZ78HMI/tPIsqdvFKHHscxR3IJuKrZI2ZoUgkt2SiufyLFBmcco+aqQBIu6P1qBsUNG3drAAGLx80vTQ== dependencies: - he "^0.5.0" - mime "^1.2.11" + he "^1.1.1" + mime "^1.6.0" minimist "^1.1.0" - url-join "^1.0.0" + url-join "^2.0.5" editions@^1.1.1, editions@^1.1.2, editions@^1.3.1, editions@^1.3.3: version "1.3.3" @@ -4656,9 +4653,10 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" +he@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== header-case@^1.0.0: version "1.0.1" @@ -4815,17 +4813,18 @@ http-proxy@^1.17.0: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-server@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.9.0.tgz#8f1b06bdc733618d4dc42831c7ba1aff4e06001a" +http-server@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b" + integrity sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w== dependencies: colors "1.0.3" corser "~2.0.0" - ecstatic "^1.4.0" + ecstatic "^3.0.0" http-proxy "^1.8.1" opener "~1.4.0" optimist "0.6.x" - portfinder "0.4.x" + portfinder "^1.0.13" union "~0.4.3" http-signature@~1.2.0: @@ -6704,12 +6703,12 @@ mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: dependencies: mime-db "~1.38.0" -mime@1.4.1, mime@^1.2.11, mime@^1.3.4: +mime@1.4.1, mime@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== -mime@^1.4.1: +mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -7860,13 +7859,6 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" -portfinder@0.4.x: - version "0.4.0" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-0.4.0.tgz#a3ffadffafe4fb98e0601a85eda27c27ce84ca1e" - dependencies: - async "0.9.0" - mkdirp "0.5.x" - portfinder@^1.0.13: version "1.0.19" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f" @@ -10540,9 +10532,10 @@ url-join@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" -url-join@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" +url-join@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" + integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= url-parse-lax@^1.0.0: version "1.0.0" From 3fecab64b1505ace2ae3a7ef5946f2506b51be17 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 9 May 2019 09:21:07 -0700 Subject: [PATCH 020/275] build: update to Bazel 0.26 (#30370) PR Close #30370 --- package.json | 2 +- packages/bazel/src/schematics/ng-add/index.ts | 2 +- yarn.lock | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 5bee6f8745..2989f92c66 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "// 3": "when updating @bazel/bazel version you also need to update the RBE settings in .bazelrc (see https://github.com/angular/angular/pull/27935)", "devDependencies": { "@angular/cli": "^8.0.0-beta.15", - "@bazel/bazel": "0.24.0", + "@bazel/bazel": "0.26.0-rc5", "@bazel/buildifier": "^0.19.2", "@bazel/ibazel": "~0.9.0", "@types/minimist": "^1.2.0", diff --git a/packages/bazel/src/schematics/ng-add/index.ts b/packages/bazel/src/schematics/ng-add/index.ts index 9a77610a55..4ebbb266c0 100755 --- a/packages/bazel/src/schematics/ng-add/index.ts +++ b/packages/bazel/src/schematics/ng-add/index.ts @@ -48,7 +48,7 @@ function addDevDependenciesToPackageJson(options: Schema) { const devDependencies: {[k: string]: string} = { '@angular/bazel': angularCoreVersion, - '@bazel/bazel': '^0.25.1', + '@bazel/bazel': '^0.26.0-rc.5', '@bazel/ibazel': '^0.10.2', '@bazel/karma': '0.27.12', '@bazel/typescript': '0.27.12', diff --git a/yarn.lock b/yarn.lock index c645cee73c..f2691cefed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -64,29 +64,29 @@ universal-analytics "^0.4.20" uuid "^3.3.2" -"@bazel/bazel-darwin_x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.24.0.tgz#828ef298d8d542961df388f17b0244f4f4302a74" - integrity sha512-xly44vkcD/fauUb7Lm5Lme4qhEZdkuuyBKSVQUHPbYAGDdbj/W8dupI3bZREkJAgG/WrRU+WXUemMj4U8ZcLcw== +"@bazel/bazel-darwin_x64@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.26.0-rc5.tgz#237a564ee03abe16ced3622624e40fb5564a9d54" + integrity sha512-1zGZUtC2jzK0bwknMaXStMmfKul15sZ089EoaWPI596VyF3uNkcOMQWDDc5daDz3ZdxpvWYbpByInmXzWQIDzw== -"@bazel/bazel-linux_x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel-linux_x64/-/bazel-linux_x64-0.24.0.tgz#9ef2e7266833ad2221fe4af4ceb6763d2897e3ff" - integrity sha512-p5ylPLWnJZDGbaIFBrtD/tp3Su5rMdzeeNJKU24XyiWQTHVZ3OD3I2Fb0ILCgfBjY8AlA7EtCtOI4hYnAuIOtg== +"@bazel/bazel-linux_x64@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel-linux_x64/-/bazel-linux_x64-0.26.0-rc5.tgz#fc10602c13ca6ecb1afe76fb2f6c2773ba1204f2" + integrity sha512-HBNn4SL6zHBjA95dY/cpjnZEVB30+L2Uq4PWw48QhuQJGY/CYDwqWk2oFMUK47EEFMFh4OQfoN3q4oHKbvixdw== -"@bazel/bazel-win32_x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel-win32_x64/-/bazel-win32_x64-0.24.0.tgz#02d83113a6c6ed99795a3e41bff5631aa141638d" - integrity sha512-/bcSEx+GoV/q7H4WM0jazfxTcurSiIIePhRv+d05mxRDcaWwhCO8KzmmZRWH1abW6npvq5tLkbSQi7G7nUBhgg== +"@bazel/bazel-win32_x64@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel-win32_x64/-/bazel-win32_x64-0.26.0-rc5.tgz#05fecc69fdf36bc883e66452e01acff89356980e" + integrity sha512-DviEGJHXKlhpvnuqfT3hjyJmGhpsKykNxnZ60dDexlh0GkhIn9rN8OF3vj0EL+pRpYhiBQvcoiwQdvV2gBYbRw== -"@bazel/bazel@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel/-/bazel-0.24.0.tgz#f4e68e3680ac299858c24c26be3d08d1151e78fc" - integrity sha512-/5E55tqH9ogAGF9Dd7RSCJmk7/xdlsPTAhsX3yEsEMs7GLdHlgD3jbeePsKUiHKKr8LXAufjTs2pXQfjrkZRMg== +"@bazel/bazel@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel/-/bazel-0.26.0-rc5.tgz#42b4ed498f02a426adee30583b98166611eb49e8" + integrity sha512-pZGUFXzvFUqm/hd7UmMPcCUc5CZd8dknae2pptQTpBhCFUHdsQeTr1alFKTNZPArVqAtqIUL2DS2MadKb3v31A== optionalDependencies: - "@bazel/bazel-darwin_x64" "0.24.0" - "@bazel/bazel-linux_x64" "0.24.0" - "@bazel/bazel-win32_x64" "0.24.0" + "@bazel/bazel-darwin_x64" "0.26.0-rc5" + "@bazel/bazel-linux_x64" "0.26.0-rc5" + "@bazel/bazel-win32_x64" "0.26.0-rc5" "@bazel/buildifier-darwin_x64@0.19.2": version "0.19.2" From 06efc340b6ad12b27d0ca1033c23aafc6bceced2 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Thu, 9 May 2019 14:51:51 -0700 Subject: [PATCH 021/275] build: update rules_nodejs and clean up bazel warnings (#30370) Preserve compatibility with rollup_bundle rule. Add missing npm dependencies, which are now enforced by the strict_deps plugin in tsc_wrapped PR Close #30370 --- WORKSPACE | 4 +- integration/bazel/WORKSPACE | 4 +- integration/bazel/src/package.json | 4 +- package.json | 7 +- packages/bazel/package.bzl | 2 + packages/bazel/src/BUILD.bazel | 1 + .../src/builders/files/WORKSPACE.template | 4 +- packages/bazel/src/esm5.bzl | 2 +- packages/bazel/src/ng_module.bzl | 4 +- packages/bazel/src/ng_package/ng_package.bzl | 7 +- packages/bazel/src/ng_rollup_bundle.bzl | 13 ++- packages/bazel/src/schematics/ng-add/index.ts | 4 +- packages/compiler-cli/BUILD.bazel | 1 + .../ngcc/test/helpers/BUILD.bazel | 1 + .../src/ngtsc/shims/test/BUILD.bazel | 1 + .../injectable-pipe/google3/BUILD.bazel | 1 + .../migrations/static-queries/BUILD.bazel | 1 + .../static-queries/google3/BUILD.bazel | 1 + .../template-var-assignment/BUILD.bazel | 1 + .../google3/BUILD.bazel | 1 + packages/core/schematics/test/BUILD.bazel | 1 + packages/core/schematics/utils/BUILD.bazel | 3 + .../core/schematics/utils/tslint/BUILD.bazel | 1 + packages/core/test/acceptance/BUILD.bazel | 1 + packages/core/test/linker/integration_spec.ts | 4 +- packages/core/test/render3/ivy/BUILD.bazel | 1 + packages/language-service/test/BUILD.bazel | 1 + tools/size-tracking/BUILD.bazel | 4 +- tools/ts-api-guardian/BUILD.bazel | 7 +- yarn.lock | 98 ++++++++++++------- 30 files changed, 123 insertions(+), 62 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 4a1fe3b291..0e652c0c25 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,8 +15,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch rules_nodejs so we can install our npm dependencies http_archive( name = "build_bazel_rules_nodejs", - sha256 = "3a3efbf223f6de733475602844ad3a8faa02abda25ab8cfe1d1ed0db134887cf", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.27.12/rules_nodejs-0.27.12.tar.gz"], + sha256 = "1db950bbd27fb2581866e307c0130983471d4c3cd49c46063a2503ca7b6770a4", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.29.0/rules_nodejs-0.29.0.tar.gz"], ) # Check the bazel version and download npm dependencies diff --git a/integration/bazel/WORKSPACE b/integration/bazel/WORKSPACE index 53f174a705..d031ac5ed3 100644 --- a/integration/bazel/WORKSPACE +++ b/integration/bazel/WORKSPACE @@ -5,8 +5,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch rules_nodejs so we can install our npm dependencies http_archive( name = "build_bazel_rules_nodejs", - sha256 = "3a3efbf223f6de733475602844ad3a8faa02abda25ab8cfe1d1ed0db134887cf", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.27.12/rules_nodejs-0.27.12.tar.gz"], + sha256 = "1db950bbd27fb2581866e307c0130983471d4c3cd49c46063a2503ca7b6770a4", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.29.0/rules_nodejs-0.29.0.tar.gz"], ) # Fetch sass rules for compiling sass files diff --git a/integration/bazel/src/package.json b/integration/bazel/src/package.json index fcbd777e1a..4da69c3a85 100644 --- a/integration/bazel/src/package.json +++ b/integration/bazel/src/package.json @@ -19,8 +19,8 @@ "@angular/bazel": "packages-dist:bazel", "@angular/compiler": "packages-dist:compiler", "@angular/compiler-cli": "packages-dist:compiler-cli", - "@bazel/karma": "0.27.12", - "@bazel/typescript": "0.27.12", + "@bazel/karma": "0.29.0", + "@bazel/typescript": "0.29.0", "@types/jasmine": "2.8.8", "@types/source-map": "0.5.1", "protractor": "5.1.2", diff --git a/package.json b/package.json index 2989f92c66..907b9a7bfb 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "@angular-devkit/core": "^8.0.0-beta.15", "@angular-devkit/schematics": "^8.0.0-beta.15", "@angular/bazel": "file:./tools/npm/@angular_bazel", - "@bazel/jasmine": "0.27.12", - "@bazel/karma": "0.27.12", - "@bazel/typescript": "0.27.12", + "@bazel/jasmine": "0.29.0", + "@bazel/karma": "0.29.0", + "@bazel/typescript": "0.29.0", "@microsoft/api-extractor": "^7.0.21", "@schematics/angular": "^8.0.0-beta.15", "@types/angular": "^1.6.47", @@ -94,6 +94,7 @@ "rollup": "^1.1.0", "rollup-plugin-amd": "^3.0.0", "rollup-plugin-commonjs": "^9.2.1", + "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^4.0.0", "rollup-plugin-sourcemaps": "^0.4.2", "rxjs": "^6.4.0", diff --git a/packages/bazel/package.bzl b/packages/bazel/package.bzl index 8d9e75f841..d010eef32e 100644 --- a/packages/bazel/package.bzl +++ b/packages/bazel/package.bzl @@ -40,12 +40,14 @@ def rules_angular_dev_dependencies(): ############################################# http_archive( name = "io_bazel_rules_sass", + sha256 = "76ae498b9a96fa029f026f8358ed44b93c934dde4691a798cb3a4137c307b7dc", strip_prefix = "rules_sass-1.15.1", url = "https://github.com/bazelbuild/rules_sass/archive/1.15.1.zip", ) http_archive( name = "io_bazel_skydoc", + sha256 = "f88058b43112e9bdc7fdb0abbdc17c5653268708c01194a159641119195e45c6", strip_prefix = "skydoc-a9550cb3ca3939cbabe3b589c57b6f531937fa99", # TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged url = "https://github.com/alexeagle/skydoc/archive/a9550cb3ca3939cbabe3b589c57b6f531937fa99.zip", diff --git a/packages/bazel/src/BUILD.bazel b/packages/bazel/src/BUILD.bazel index 4dba862e0a..3e0adfee11 100644 --- a/packages/bazel/src/BUILD.bazel +++ b/packages/bazel/src/BUILD.bazel @@ -18,6 +18,7 @@ nodejs_binary( "@npm//rollup", "@npm//rollup-plugin-amd", "@npm//rollup-plugin-commonjs", + "@npm//rollup-plugin-json", "@npm//rollup-plugin-node-resolve", "@npm//rollup-plugin-sourcemaps", ], diff --git a/packages/bazel/src/builders/files/WORKSPACE.template b/packages/bazel/src/builders/files/WORKSPACE.template index d87aa93005..cb7bf141f2 100644 --- a/packages/bazel/src/builders/files/WORKSPACE.template +++ b/packages/bazel/src/builders/files/WORKSPACE.template @@ -12,8 +12,8 @@ workspace(name = "project") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -RULES_NODEJS_VERSION = "0.27.12" -RULES_NODEJS_SHA256 = "3a3efbf223f6de733475602844ad3a8faa02abda25ab8cfe1d1ed0db134887cf" +RULES_NODEJS_VERSION = "0.29.0" +RULES_NODEJS_SHA256 = "1db950bbd27fb2581866e307c0130983471d4c3cd49c46063a2503ca7b6770a4" http_archive( name = "build_bazel_rules_nodejs", sha256 = RULES_NODEJS_SHA256, diff --git a/packages/bazel/src/esm5.bzl b/packages/bazel/src/esm5.bzl index 57d2157ca3..98463cb168 100644 --- a/packages/bazel/src/esm5.bzl +++ b/packages/bazel/src/esm5.bzl @@ -108,7 +108,7 @@ def _esm5_outputs_aspect(target, ctx): ctx.actions.run( progress_message = "Compiling TypeScript (ES5 with ES Modules) %s" % target.label, - inputs = target.typescript.replay_params.inputs + [tsconfig], + inputs = target.typescript.replay_params.inputs.to_list() + [tsconfig], outputs = outputs, arguments = [tsconfig.path], executable = compiler, diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 144fdf08d9..3504c16be4 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -427,7 +427,7 @@ def ngc_compile_action( if is_legacy_ngc and messages_out != None: ctx.actions.run( - inputs = list(inputs), + inputs = inputs, outputs = messages_out, executable = ctx.executable.ng_xi18n, arguments = (_EXTRA_NODE_OPTIONS_FLAGS + @@ -442,7 +442,7 @@ def ngc_compile_action( if dts_bundles_out != None: # combine the inputs and outputs and filter .d.ts and json files - filter_inputs = [f for f in inputs + outputs if f.path.endswith(".d.ts") or f.path.endswith(".json")] + filter_inputs = [f for f in list(inputs) + outputs if f.path.endswith(".d.ts") or f.path.endswith(".json")] if _should_produce_flat_module_outs(ctx): dts_entry_points = ["%s.d.ts" % _flat_module_out_file(ctx)] diff --git a/packages/bazel/src/ng_package/ng_package.bzl b/packages/bazel/src/ng_package/ng_package.bzl index f1a3c7a788..c00b44bec0 100644 --- a/packages/bazel/src/ng_package/ng_package.bzl +++ b/packages/bazel/src/ng_package/ng_package.bzl @@ -289,12 +289,13 @@ def _ng_package_impl(ctx): for d in ctx.attr.deps: if NodeModuleInfo in d: node_modules_files += _filter_js_inputs(d.files) + esm5_rollup_inputs = depset(node_modules_files, transitive = [esm5_sources]) esm2015_config = write_rollup_config(ctx, [], "/".join([ctx.bin_dir.path, ctx.label.package, _esm2015_root_dir(ctx)]), filename = "_%s.rollup_esm2015.conf.js") esm5_config = write_rollup_config(ctx, [], "/".join([ctx.bin_dir.path, ctx.label.package, esm5_root_dir(ctx)]), filename = "_%s.rollup_esm5.conf.js") - fesm2015.append(_rollup(ctx, "fesm2015", esm2015_config, es2015_entry_point, esm_2015_files + node_modules_files, fesm2015_output)) - fesm5.append(_rollup(ctx, "fesm5", esm5_config, es5_entry_point, esm5_sources + node_modules_files, fesm5_output)) + fesm2015.append(_rollup(ctx, "fesm2015", esm2015_config, es2015_entry_point, depset(node_modules_files, transitive = [esm_2015_files]), fesm2015_output)) + fesm5.append(_rollup(ctx, "fesm5", esm5_config, es5_entry_point, esm5_rollup_inputs, fesm5_output)) bundles.append( _rollup( @@ -302,7 +303,7 @@ def _ng_package_impl(ctx): "umd", esm5_config, es5_entry_point, - esm5_sources + node_modules_files, + esm5_rollup_inputs, umd_output, format = "umd", package_name = package_name, diff --git a/packages/bazel/src/ng_rollup_bundle.bzl b/packages/bazel/src/ng_rollup_bundle.bzl index d1100520e6..46f9641ead 100644 --- a/packages/bazel/src/ng_rollup_bundle.bzl +++ b/packages/bazel/src/ng_rollup_bundle.bzl @@ -18,7 +18,6 @@ load( "@build_bazel_rules_nodejs//internal/rollup:rollup_bundle.bzl", "ROLLUP_ATTRS", "ROLLUP_DEPS_ASPECTS", - "ROLLUP_OUTPUTS", "run_rollup", "run_sourcemapexplorer", "run_terser", @@ -27,6 +26,18 @@ load( load("@build_bazel_rules_nodejs//internal/common:collect_es6_sources.bzl", collect_es2015_sources = "collect_es6_sources") load(":esm5.bzl", "esm5_outputs_aspect", "esm5_root_dir", "flatten_esm5") +ROLLUP_OUTPUTS = { + "build_cjs": "%{name}.cjs.js", + "build_es2015": "%{name}.es2015.js", + "build_es2015_min": "%{name}.min.es2015.js", + "build_es2015_min_debug": "%{name}.min_debug.es2015.js", + "build_es5": "%{name}.js", + "build_es5_min": "%{name}.min.js", + "build_es5_min_debug": "%{name}.min_debug.js", + "build_umd": "%{name}.umd.js", + "explore_html": "%{name}.explore.html", +} + PACKAGES = [ # Generated paths when using ng_rollup_bundle outside this monorepo. "external/angular/packages/core/src", diff --git a/packages/bazel/src/schematics/ng-add/index.ts b/packages/bazel/src/schematics/ng-add/index.ts index 4ebbb266c0..6bca362fe3 100755 --- a/packages/bazel/src/schematics/ng-add/index.ts +++ b/packages/bazel/src/schematics/ng-add/index.ts @@ -50,8 +50,8 @@ function addDevDependenciesToPackageJson(options: Schema) { '@angular/bazel': angularCoreVersion, '@bazel/bazel': '^0.26.0-rc.5', '@bazel/ibazel': '^0.10.2', - '@bazel/karma': '0.27.12', - '@bazel/typescript': '0.27.12', + '@bazel/karma': '0.29.0', + '@bazel/typescript': '0.29.0', }; const recorder = host.beginUpdate(packageJson); diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 4ceb6afa7b..2c1eab6262 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -43,6 +43,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/util", "@npm//@bazel/typescript", "@npm//@types", + "@npm//reflect-metadata", "@npm//tsickle", "@npm//typescript", ], diff --git a/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel b/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel index 012a925b94..cc237359fe 100644 --- a/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel @@ -12,5 +12,6 @@ ts_library( "//packages/compiler-cli/ngcc", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/testing", + "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel index 381f9cdd88..de435dc818 100644 --- a/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( deps = [ "//packages:types", "//packages/compiler-cli/src/ngtsc/shims", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel b/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel index 773d18ed6d..ffe09d7d9c 100644 --- a/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel @@ -8,5 +8,6 @@ ts_library( deps = [ "//packages/core/schematics/migrations/injectable-pipe", "@npm//tslint", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/migrations/static-queries/BUILD.bazel b/packages/core/schematics/migrations/static-queries/BUILD.bazel index f8fa0a26ba..25993f3bf4 100644 --- a/packages/core/schematics/migrations/static-queries/BUILD.bazel +++ b/packages/core/schematics/migrations/static-queries/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli", "//packages/core/schematics/utils", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/node", "@npm//rxjs", diff --git a/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel b/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel index a588549552..9467244813 100644 --- a/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel @@ -9,5 +9,6 @@ ts_library( "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/utils", "@npm//tslint", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel b/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel index a45b58f070..5e0003affc 100644 --- a/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel +++ b/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( deps = [ "//packages/compiler", "//packages/core/schematics/utils", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/node", "@npm//typescript", diff --git a/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel b/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel index 83b75a91e6..2beb6d06a4 100644 --- a/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel @@ -10,5 +10,6 @@ ts_library( "//packages/core/schematics/utils", "//packages/core/schematics/utils/tslint", "@npm//tslint", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index f61d48e0dc..b064f3a3af 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -17,6 +17,7 @@ ts_library( "//packages/core/schematics/migrations/template-var-assignment", "//packages/core/schematics/migrations/template-var-assignment/google3", "//packages/core/schematics/utils", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/shelljs", "@npm//tslint", diff --git a/packages/core/schematics/utils/BUILD.bazel b/packages/core/schematics/utils/BUILD.bazel index 44b3e073e8..5ebf78b9b7 100644 --- a/packages/core/schematics/utils/BUILD.bazel +++ b/packages/core/schematics/utils/BUILD.bazel @@ -9,5 +9,8 @@ ts_library( "//packages/compiler", "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", + "@npm//@schematics/angular", + "@npm//@types/node", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/utils/tslint/BUILD.bazel b/packages/core/schematics/utils/tslint/BUILD.bazel index 320800a510..cae69eca25 100644 --- a/packages/core/schematics/utils/tslint/BUILD.bazel +++ b/packages/core/schematics/utils/tslint/BUILD.bazel @@ -7,4 +7,5 @@ ts_library( visibility = [ "//packages/core/schematics/migrations/template-var-assignment/google3:__pkg__", ], + deps = ["@npm//typescript"], ) diff --git a/packages/core/test/acceptance/BUILD.bazel b/packages/core/test/acceptance/BUILD.bazel index c037d23381..19c700031d 100644 --- a/packages/core/test/acceptance/BUILD.bazel +++ b/packages/core/test/acceptance/BUILD.bazel @@ -24,6 +24,7 @@ ts_library( "//packages/platform-browser/testing", "//packages/platform-server", "//packages/private/testing", + "@npm//rxjs", "@npm//zone.js", ], ) diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 2687a54e6a..b6bb694786 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1522,7 +1522,7 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('INPUT'); expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); expect((c.injector).get).toBeTruthy(); - expect(c.context).toBe(fixture.componentInstance); + expect(c.context).toEqual(fixture.componentInstance); expect(c.references['local']).toBeDefined(); } }); @@ -1567,7 +1567,7 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN'); expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); expect((c.injector).get).toBeTruthy(); - expect(c.context).toBe(fixture.componentInstance); + expect(c.context).toEqual(fixture.componentInstance); expect(c.references['local']).toBeDefined(); })); }); diff --git a/packages/core/test/render3/ivy/BUILD.bazel b/packages/core/test/render3/ivy/BUILD.bazel index 39e0a8ce87..f5f2ecc499 100644 --- a/packages/core/test/render3/ivy/BUILD.bazel +++ b/packages/core/test/render3/ivy/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( "//packages:types", "//packages/core", "//packages/core/src/di/interface", + "@npm//reflect-metadata", ], ) diff --git a/packages/language-service/test/BUILD.bazel b/packages/language-service/test/BUILD.bazel index 0348a26aac..4529a90fa3 100644 --- a/packages/language-service/test/BUILD.bazel +++ b/packages/language-service/test/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/test:test_utils", "//packages/language-service", + "@npm//reflect-metadata", "@npm//typescript", ], ) diff --git a/tools/size-tracking/BUILD.bazel b/tools/size-tracking/BUILD.bazel index 7a040f4832..b01bed4829 100644 --- a/tools/size-tracking/BUILD.bazel +++ b/tools/size-tracking/BUILD.bazel @@ -12,6 +12,8 @@ ts_library( deps = [ "@npm//@types/node", "@npm//@types/source-map", + "@npm//chalk", + "@npm//source-map", ], ) @@ -21,7 +23,7 @@ ts_library( srcs = glob(["**/*_spec.ts"]), deps = [ ":size-tracking", - "@npm//@types/source-map", + "@npm//source-map", ], ) diff --git a/tools/ts-api-guardian/BUILD.bazel b/tools/ts-api-guardian/BUILD.bazel index b830f9842f..cdf3db75d0 100644 --- a/tools/ts-api-guardian/BUILD.bazel +++ b/tools/ts-api-guardian/BUILD.bazel @@ -1,9 +1,6 @@ # BEGIN-INTERNAL -load( - "@build_bazel_rules_nodejs//:defs.bzl", - "jasmine_node_test", - "npm_package", -) +load("@build_bazel_rules_nodejs//:defs.bzl", "npm_package") +load("@npm_bazel_jasmine//:index.bzl", "jasmine_node_test") load("@npm_bazel_typescript//:index.bzl", "ts_library") ts_library( diff --git a/yarn.lock b/yarn.lock index f2691cefed..1907ddbbfd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -111,24 +111,25 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.9.0.tgz#fd60023acd36313d304cc2f8c2e181b88b5445cd" integrity sha512-E31cefDcdJsx/oii6p/gqKZXSVw0kEg1O73DD2McFcSvnf/p1GYWcQtVgdRQmlviBEytJkJgdX8rtThitRvcow== -"@bazel/jasmine@0.27.12": - version "0.27.12" - resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-0.27.12.tgz#9319d1fb52723da8ec23d4ce507dc9eb202d4e31" - integrity sha512-olsKMLsfqA6F025EPn0glJal4DB0v4E2ISL+0Hu2piewAtjv8DARU0dxzyizgH1X20BooIlgq+ZL2cKdlXfMHg== +"@bazel/jasmine@0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-0.29.0.tgz#347d9c512daf8576dcdaa9bb168733fa444c2263" + integrity sha512-QH/mLAH4e7gcJrfOT0BmJ4wk+5Ly3RU+RPLaCyacnCjmJCICukZJa/rrjbVtwd8u7ZM+Hf6tRaLLydSeKXGBug== dependencies: jasmine "~3.3.1" - v8-coverage "1.0.8" + jasmine-core "~3.3.0" + v8-coverage "1.0.9" -"@bazel/karma@0.27.12": - version "0.27.12" - resolved "https://registry.yarnpkg.com/@bazel/karma/-/karma-0.27.12.tgz#a7c1648af93b4d376e4e3b6fc1c7006b4d79c092" - integrity sha512-LJAbNe8bR1vRLb8GqIbKzCpf8VMaTnM9jizxmsjkgdGjy9OgsseYbk6+70z1yx7s3QHPjj7JBqcx+d2yeXPo7g== +"@bazel/karma@0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@bazel/karma/-/karma-0.29.0.tgz#bcb79750ecec3884136e584b36e3cd3b610455a5" + integrity sha512-dSN7iBAz0srGkJxAP7NAyWVdCWoUHBxlGL7YxHObV3kDTwWUDKWKcEwuOC7MTMSQtYM4WcVVvfCO3HhXKUxhPg== dependencies: jasmine-core "2.8.0" karma "^4.0.0" karma-chrome-launcher "2.2.0" karma-firefox-launcher "1.1.0" - karma-jasmine "1.1.1" + karma-jasmine "2.0.1" karma-requirejs "1.1.0" karma-sauce-launcher "2.0.2" karma-sourcemap-loader "0.3.7" @@ -136,10 +137,10 @@ semver "5.6.0" tmp "0.0.33" -"@bazel/typescript@0.27.12": - version "0.27.12" - resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.27.12.tgz#79a6468cd8da096c4da151fac658f7f801a59948" - integrity sha512-5cH+x7rvO8P9MCrd8YwJrTyzkET6MLajzngoV5yLDWwcokzs+b3yD9yoa5Vw3Dth2MdKRp+lyGMO7PwyAM3ebw== +"@bazel/typescript@0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.29.0.tgz#d276afe034f37b5f35ee1369c99dc33c637fc9f6" + integrity sha512-Dp5ucrE1vXTORGiwEi6Ur4dlICpLsmZ1dscsEQT4ywF7xTT0/NmIG0ecBghiCFPFQTxt1D05TR3SH06rPtTAew== dependencies: protobufjs "6.8.8" semver "5.6.0" @@ -6034,6 +6035,11 @@ jasmine-core@^3.1.0: resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.1.0.tgz#a4785e135d5df65024dfc9224953df585bd2766c" integrity sha1-pHheE11d9lAk38kiSVPfWFvSdmw= +jasmine-core@^3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.4.0.tgz#2a74618e966026530c3518f03e9f845d26473ce3" + integrity sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg== + jasmine-core@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.3.0.tgz#dea1cdc634bc93c7e0d4ad27185df30fa971b10e" @@ -6370,10 +6376,12 @@ karma-firefox-launcher@1.1.0: resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz#2c47030452f04531eb7d13d4fc7669630bb93339" integrity sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA== -karma-jasmine@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.1.tgz#6fe840e75a11600c9d91e84b33c458e1c46a3529" - integrity sha1-b+hA51oRYAydkehLM8RY4cRqNSk= +karma-jasmine@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-2.0.1.tgz#26e3e31f2faf272dd80ebb0e1898914cc3a19763" + integrity sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA== + dependencies: + jasmine-core "^3.3" karma-jasmine@^1.1.2: version "1.1.2" @@ -7726,7 +7734,7 @@ node-source-walk@~1.4.0: dependencies: acorn "^1.0.3" -node-uuid@1.4.8, node-uuid@^1.4.8, node-uuid@~1.4.7: +node-uuid@1.4.8, node-uuid@~1.4.7: version "1.4.8" resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= @@ -8888,6 +8896,14 @@ read-pkg-up@^3.0.0: find-up "^2.0.0" read-pkg "^3.0.0" +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + read-pkg@^1.0.0, read-pkg@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -9389,6 +9405,13 @@ rollup-plugin-commonjs@^9.2.1: resolve "^1.10.0" rollup-pluginutils "^2.3.3" +rollup-plugin-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz#a18da0a4b30bf5ca1ee76ddb1422afbb84ae2b9e" + integrity sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow== + dependencies: + rollup-pluginutils "^2.5.0" + rollup-plugin-node-resolve@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.1.tgz#f95765d174e5daeef9ea6268566141f53aa9d422" @@ -9422,6 +9445,14 @@ rollup-pluginutils@^2.3.3: estree-walker "^0.6.0" micromatch "^3.1.10" +rollup-pluginutils@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.6.0.tgz#203706edd43dfafeaebc355d7351119402fc83ad" + integrity sha512-aGQwspEF8oPKvg37u3p7h0cYNwmJR1sCBMZGZ5b9qy8HGtETknqjzcxrDRrcAnJNXN18lBH4Q9vZYth/p4n8jQ== + dependencies: + estree-walker "^0.6.0" + micromatch "^3.1.10" + rollup@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.1.0.tgz#461a7534b55be48aa4a6e6810a1543a5769e75d1" @@ -10528,16 +10559,15 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" -test-exclude@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" - integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== +test-exclude@^5.2.2: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== dependencies: - arrify "^1.0.1" - micromatch "^2.3.11" - object-assign "^4.1.0" - read-pkg-up "^1.0.1" - require-main-filename "^1.0.1" + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" text-extensions@^1.0.0: version "1.7.0" @@ -11233,10 +11263,10 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -v8-coverage@1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/v8-coverage/-/v8-coverage-1.0.8.tgz#3393cb904cd064e2e56e747781641c75a6e7f52c" - integrity sha512-DWNS16h1LKyRMZsJ7+2KFUhA4hGbWUWTcUbDwnT6WMQKbolixY1KCSUaw2NVqtpwODGtqCWvHUjQjwfh562U0A== +v8-coverage@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/v8-coverage/-/v8-coverage-1.0.9.tgz#780889680c0fea0f587adf22e2b5f443b9434745" + integrity sha512-JolsCH1JDI2QULrxkAGZaovJPvg/Q0p20Uj0F5N8fPtYDtz38gNBRPQ/WVXlLLd3d8WHvKN96AfE4XFk4u0g2g== dependencies: debug "^3.1.0" foreground-child "^1.5.6" @@ -11244,11 +11274,11 @@ v8-coverage@1.0.8: istanbul-lib-report "^1.1.3" istanbul-reports "^1.3.0" mkdirp "^0.5.1" - node-uuid "^1.4.8" rimraf "^2.6.2" signal-exit "^3.0.2" spawn-wrap "^1.4.2" - test-exclude "^4.2.1" + test-exclude "^5.2.2" + uuid "^3.3.2" v8-to-istanbul "1.2.0" yargs "^11.0.0" From c7f9a95a3f2cd5490ab2e5c174e62a1b7736cae0 Mon Sep 17 00:00:00 2001 From: Alan Date: Tue, 14 May 2019 14:21:30 +0200 Subject: [PATCH 022/275] test: fix tests in windows ci (#30451) PR Close #30451 --- .../ngcc/test/packages/entry_point_bundle_spec.ts | 8 +++++--- packages/compiler-cli/src/ngtsc/path/src/logical.ts | 2 +- packages/compiler-cli/test/ngtsc/env.ts | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts index b976f9487a..4049ac9c25 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts @@ -5,10 +5,12 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; import {MockFileSystem} from '../helpers/mock_file_system'; +const _ = AbsoluteFsPath.from; + function createMockFileSystem() { return new MockFileSystem({ '/node_modules/test': { @@ -102,7 +104,7 @@ describe('entry point bundle', () => { // Modules resolved from "other" should be declaration files '/node_modules/other/public_api.d.ts', '/node_modules/other/index.d.ts', - ])); + ].map(p => _(p).toString()))); expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName)) .toEqual(jasmine.arrayWithExactContents([ @@ -115,6 +117,6 @@ describe('entry point bundle', () => { '/node_modules/test/secondary/index.d.ts', '/node_modules/other/public_api.d.ts', '/node_modules/other/index.d.ts', - ])); + ].map(p => _(p).toString()))); }); }); diff --git a/packages/compiler-cli/src/ngtsc/path/src/logical.ts b/packages/compiler-cli/src/ngtsc/path/src/logical.ts index e00a74f4ed..4ad75a3f59 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/logical.ts +++ b/packages/compiler-cli/src/ngtsc/path/src/logical.ts @@ -67,7 +67,7 @@ export class LogicalFileSystem { * `logicalPathOfFile(AbsoluteFsPath.fromSourceFile(sf))`. */ logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null { - return this.logicalPathOfFile(sf.fileName as AbsoluteFsPath); + return this.logicalPathOfFile(AbsoluteFsPath.from(sf.fileName)); } /** diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 98e5f4bd0e..3daec54ab5 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -49,7 +49,7 @@ export class NgtscTestEnvironment { */ static setup(): NgtscTestEnvironment { const support = setup(); - const outDir = path.join(support.basePath, 'built'); + const outDir = path.posix.join(support.basePath, 'built'); process.chdir(support.basePath); setupFakeCore(support); @@ -133,7 +133,7 @@ export class NgtscTestEnvironment { write(fileName: string, content: string) { if (this.multiCompileHostExt !== null) { - const absFilePath = path.resolve(this.support.basePath, fileName); + const absFilePath = path.posix.resolve(this.support.basePath, fileName); this.multiCompileHostExt.invalidate(absFilePath); } this.support.write(fileName, content); @@ -143,7 +143,7 @@ export class NgtscTestEnvironment { if (this.multiCompileHostExt === null) { throw new Error(`Not caching files - call enableMultipleCompilations()`); } - const fullFile = path.join(this.support.basePath, fileName); + const fullFile = path.posix.join(this.support.basePath, fileName); this.multiCompileHostExt.invalidate(fullFile); } From ff29cccb777b517149fca4aa44048a061c3fcebb Mon Sep 17 00:00:00 2001 From: Keen Yee Liau Date: Mon, 13 May 2019 12:47:58 -0700 Subject: [PATCH 023/275] fix(bazel): Use existing npm/yarn lock files (#30438) This PR fixes Bazel builder to create yarn_install rule in WORKSPACE if yarn.lock is present, otherwise npm_install rule if package-lock.json is present. If none is present, default to yarn_install and create an empty yarn.lock file. PR closes https://github.com/angular/angular/issues/30164 PR closes https://github.com/angular/angular/pull/30359 PR Close #30438 --- packages/bazel/src/builders/bazel.ts | 36 ++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/bazel/src/builders/bazel.ts b/packages/bazel/src/builders/bazel.ts index 5a15c6503f..a9f143e453 100644 --- a/packages/bazel/src/builders/bazel.ts +++ b/packages/bazel/src/builders/bazel.ts @@ -9,7 +9,7 @@ /// import {spawn} from 'child_process'; -import {copyFileSync, existsSync, readdirSync, statSync, unlinkSync} from 'fs'; +import {copyFileSync, existsSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync} from 'fs'; import {dirname, join, normalize} from 'path'; export type Executable = 'bazel' | 'ibazel'; @@ -106,6 +106,33 @@ function listR(dir: string): string[] { return list(dir, '', []); } +/** + * Return the name of the lock file that is present in the specified 'root' + * directory. If none exists, default to creating an empty yarn.lock file. + */ +function getOrCreateLockFile(root: string): 'yarn.lock'|'package-lock.json' { + const yarnLock = join(root, 'yarn.lock'); + if (existsSync(yarnLock)) { + return 'yarn.lock'; + } + const npmLock = join(root, 'package-lock.json'); + if (existsSync(npmLock)) { + return 'package-lock.json'; + } + // Prefer yarn if no lock file exists + writeFileSync(yarnLock, ''); + return 'yarn.lock'; +} + +// Replace yarn_install rule with npm_install and copy from 'source' to 'dest'. +function replaceYarnWithNpm(source: string, dest: string) { + const srcContent = readFileSync(source, 'utf-8'); + const destContent = srcContent.replace(/yarn_install/g, 'npm_install') + .replace('yarn_lock', 'package_lock_json') + .replace('yarn.lock', 'package-lock.json'); + writeFileSync(dest, destContent); +} + /** * Copy Bazel files (WORKSPACE, BUILD.bazel, etc) from the template directory to * the project `root` directory, and return the absolute paths of the files @@ -117,6 +144,7 @@ export function copyBazelFiles(root: string, templateDir: string) { templateDir = normalize(templateDir); const bazelFiles: string[] = []; const templates = listR(templateDir); + const useYarn = getOrCreateLockFile(root) === 'yarn.lock'; for (const template of templates) { const name = template.replace('__dot__', '.').replace('.template', ''); @@ -124,7 +152,11 @@ export function copyBazelFiles(root: string, templateDir: string) { const dest = join(root, name); try { if (!existsSync(dest)) { - copyFileSync(source, dest); + if (!useYarn && name === 'WORKSPACE') { + replaceYarnWithNpm(source, dest); + } else { + copyFileSync(source, dest); + } bazelFiles.push(dest); } } catch { From 1b059e595fd6a773b3715d079b75ef6c40d69d9f Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Tue, 30 Apr 2019 14:07:48 +0200 Subject: [PATCH 024/275] test(ivy): move common integration tests to acceptance (#30213) PR Close #30213 --- .../acceptance/common_integration_spec.ts | 541 ++++++++ .../test/render3/common_integration_spec.ts | 1094 ----------------- 2 files changed, 541 insertions(+), 1094 deletions(-) create mode 100644 packages/core/test/acceptance/common_integration_spec.ts delete mode 100644 packages/core/test/render3/common_integration_spec.ts diff --git a/packages/core/test/acceptance/common_integration_spec.ts b/packages/core/test/acceptance/common_integration_spec.ts new file mode 100644 index 0000000000..14615fb8f6 --- /dev/null +++ b/packages/core/test/acceptance/common_integration_spec.ts @@ -0,0 +1,541 @@ +/** + * @license + * Copyright Google Inc. 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 {Component, Directive} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +describe('@angular/common integration', () => { + + describe('NgForOf', () => { + @Directive({selector: '[dir]'}) + class MyDirective { + } + + @Component({selector: 'app-child', template: '
    comp text
    '}) + class ChildComponent { + } + + @Component({selector: 'app-root', template: ''}) + class AppComponent { + items: string[] = ['first', 'second']; + } + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [AppComponent, ChildComponent, MyDirective]}); + }); + + it('should update a loop', () => { + TestBed.overrideTemplate( + AppComponent, '
    • {{item}}
    '); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first', 'second']); + + // change detection cycle, no model changes + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first', 'second']); + + // remove the last item + const items = fixture.componentInstance.items; + items.length = 1; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first']); + + // change an item + items[0] = 'one'; + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['one']); + + // add an item + items.push('two'); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['one', 'two']); + }); + + it('should support ngForOf context variables', () => { + TestBed.overrideTemplate( + AppComponent, + '
    • {{myIndex}} of {{myCount}}: {{item}}
    '); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['0 of 2: first', '1 of 2: second']); + + // add an item in the middle + const items = fixture.componentInstance.items; + items.splice(1, 0, 'middle'); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual([ + '0 of 3: first', '1 of 3: middle', '2 of 3: second' + ]); + }); + + it('should instantiate directives inside directives properly in an ngFor', () => { + TestBed.overrideTemplate(AppComponent, ''); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + const children = fixture.debugElement.queryAll(By.directive(ChildComponent)); + + // expect 2 children, each one with a directive + expect(children.length).toBe(2); + expect(children.map(child => child.nativeElement.innerHTML)).toEqual([ + '
    comp text
    ', '
    comp text
    ' + ]); + let directive = children[0].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + directive = children[1].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + + // add an item + const items = fixture.componentInstance.items; + items.push('third'); + fixture.detectChanges(); + + const childrenAfterAdd = fixture.debugElement.queryAll(By.directive(ChildComponent)); + + expect(childrenAfterAdd.length).toBe(3); + expect(childrenAfterAdd.map(child => child.nativeElement.innerHTML)).toEqual([ + '
    comp text
    ', '
    comp text
    ', '
    comp text
    ' + ]); + directive = childrenAfterAdd[2].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + }); + + it('should retain parent view listeners when the NgFor destroy views', () => { + @Component({ + selector: 'app-toggle', + template: ` +
      +
    • {{item}}
    • +
    ` + }) + class ToggleComponent { + private _data: number[] = [1, 2, 3]; + items: number[] = []; + + toggle() { + if (this.items.length) { + this.items = []; + } else { + this.items = this._data; + } + } + } + + TestBed.configureTestingModule({declarations: [ToggleComponent]}); + const fixture = TestBed.createComponent(ToggleComponent); + fixture.detectChanges(); + + // no elements in the list + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(0); + + // this will fill the list + fixture.componentInstance.toggle(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(3); + expect(listItems.map(li => li.textContent)).toEqual(['1', '2', '3']); + + // now toggle via the button + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(0); + + // toggle again + button.click(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(3); + }); + + it('should support multiple levels of embedded templates', () => { + @Component({ + selector: 'app-multi', + template: `
      +
    • + {{cell}} - {{ row.value }} - {{ items.length }} +
    • +
    ` + }) + class MultiLevelComponent { + items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; + } + + TestBed.configureTestingModule({declarations: [MultiLevelComponent]}); + const fixture = TestBed.createComponent(MultiLevelComponent); + fixture.detectChanges(); + + // change detection cycle, no model changes + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(2); + let spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['1 - first - 2', '2 - first - 2']); + spanItems = Array.from(listItems[1].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['3 - second - 2', '4 - second - 2']); + + // remove the last item + const items = fixture.componentInstance.items; + items.length = 1; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(1); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['1 - first - 1', '2 - first - 1']); + + // change an item + items[0].data[0] = 'one'; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(1); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['one - first - 1', '2 - first - 1']); + + // add an item + items[1] = {data: ['three', '4'], value: 'third'}; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(2); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['one - first - 2', '2 - first - 2']); + spanItems = Array.from(listItems[1].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual([ + 'three - third - 2', '4 - third - 2' + ]); + }); + + it('should support multiple levels of embedded templates with listeners', () => { + @Component({ + selector: 'app-multi', + template: `
    +

    + + {{ row.value }} - {{ name }} +

    +
    ` + }) + class MultiLevelWithListenerComponent { + items: any[] = [{data: ['1'], value: 'first'}]; + name = 'app'; + events: string[] = []; + + onClick(value: string, name: string) { this.events.push(value, name); } + } + + TestBed.configureTestingModule({declarations: [MultiLevelWithListenerComponent]}); + const fixture = TestBed.createComponent(MultiLevelWithListenerComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('p'); + expect(elements.length).toBe(1); + expect(elements[0].innerHTML).toBe(' first - app '); + + const span: HTMLSpanElement = fixture.nativeElement.querySelector('span'); + span.click(); + expect(fixture.componentInstance.events).toEqual(['first', 'app']); + + fixture.componentInstance.name = 'new name'; + fixture.detectChanges(); + expect(elements[0].innerHTML).toBe(' first - new name '); + + span.click(); + expect(fixture.componentInstance.events).toEqual(['first', 'app', 'first', 'new name']); + }); + + it('should support skipping contexts', () => { + @Component({ + selector: 'app-multi', + template: `
    +
    + {{ cell.value }} - {{ name }} +
    +
    ` + }) + class SkippingContextComponent { + name = 'app'; + items: any[] = [ + [ + // row + {value: 'one', data: ['1', '2']} // cell + ], + [{value: 'two', data: ['3', '4']}] + ]; + } + + TestBed.configureTestingModule({declarations: [SkippingContextComponent]}); + const fixture = TestBed.createComponent(SkippingContextComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('span'); + expect(elements.length).toBe(4); + expect(elements[0].textContent).toBe('one - app'); + expect(elements[1].textContent).toBe('one - app'); + expect(elements[2].textContent).toBe('two - app'); + expect(elements[3].textContent).toBe('two - app'); + + fixture.componentInstance.name = 'other'; + fixture.detectChanges(); + expect(elements[0].textContent).toBe('one - other'); + expect(elements[1].textContent).toBe('one - other'); + expect(elements[2].textContent).toBe('two - other'); + expect(elements[3].textContent).toBe('two - other'); + }); + + it('should support context for 9+ levels of embedded templates', () => { + @Component({ + selector: 'app-multi', + template: `
    + + + + + + + + {{ item8 }}.{{ item7.value }}.{{ item6.value }}.{{ item5.value }}.{{ item4.value }}.{{ item3.value }}.{{ item2.value }}.{{ item1.value }}.{{ item0.value }}.{{ value }} + + + + + + + +
    ` + }) + class NineLevelsComponent { + value = 'App'; + items: any[] = [ + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '1', '2' // item8 + ], + value: 'h' + }], + value: 'g' + }], + value: 'f' + }], + value: 'e' + }], + value: 'd' + }], + value: 'c' + }], + value: 'b' + }], + value: 'a' + }, + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '3', '4' // item8 + ], + value: 'H' + }], + value: 'G' + }], + value: 'F' + }], + value: 'E' + }], + value: 'D' + }], + value: 'C' + }], + value: 'B' + }], + value: 'A' + } + ]; + } + + TestBed.configureTestingModule({declarations: [NineLevelsComponent]}); + const fixture = TestBed.createComponent(NineLevelsComponent); + fixture.detectChanges(); + + const divItems = (fixture.nativeElement as HTMLElement).querySelectorAll('div'); + expect(divItems.length).toBe(2); // 2 outer loops + let spanItems = + divItems[0].querySelectorAll('span > span > span > span > span > span > span > span'); + expect(spanItems.length).toBe(2); // 2 inner elements + expect(spanItems[0].textContent).toBe('1.h.g.f.e.d.c.b.a.App'); + expect(spanItems[1].textContent).toBe('2.h.g.f.e.d.c.b.a.App'); + spanItems = + divItems[1].querySelectorAll('span > span > span > span > span > span > span > span'); + expect(spanItems.length).toBe(2); // 2 inner elements + expect(spanItems[0].textContent).toBe('3.H.G.F.E.D.C.B.A.App'); + expect(spanItems[1].textContent).toBe('4.H.G.F.E.D.C.B.A.App'); + }); + }); + + describe('ngIf', () => { + it('should support sibling ngIfs', () => { + @Component({ + selector: 'app-multi', + template: ` +
    {{ valueOne }}
    +
    {{ valueTwo }}
    + ` + }) + class SimpleConditionComponent { + showing = true; + valueOne = 'one'; + valueTwo = 'two'; + } + + TestBed.configureTestingModule({declarations: [SimpleConditionComponent]}); + const fixture = TestBed.createComponent(SimpleConditionComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('div'); + expect(elements.length).toBe(2); + expect(elements[0].textContent).toBe('one'); + expect(elements[1].textContent).toBe('two'); + + fixture.componentInstance.valueOne = '$$one$$'; + fixture.componentInstance.valueTwo = '$$two$$'; + fixture.detectChanges(); + expect(elements[0].textContent).toBe('$$one$$'); + expect(elements[1].textContent).toBe('$$two$$'); + }); + + it('should handle nested ngIfs with no intermediate context vars', () => { + @Component({ + selector: 'app-multi', + template: `
    +
    +
    {{ name }}
    +
    +
    + ` + }) + class NestedConditionsComponent { + showing = true; + outerShowing = true; + innerShowing = true; + name = 'App name'; + } + + TestBed.configureTestingModule({declarations: [NestedConditionsComponent]}); + const fixture = TestBed.createComponent(NestedConditionsComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('div'); + expect(elements.length).toBe(3); + expect(elements[2].textContent).toBe('App name'); + + fixture.componentInstance.name = 'Other name'; + fixture.detectChanges(); + expect(elements[2].textContent).toBe('Other name'); + }); + }); + + describe('NgTemplateOutlet', () => { + + it('should create and remove embedded views', () => { + @Component({ + selector: 'app-multi', + template: `from tpl + + ` + }) + class EmbeddedViewsComponent { + showing = false; + } + + TestBed.configureTestingModule({declarations: [EmbeddedViewsComponent]}); + const fixture = TestBed.createComponent(EmbeddedViewsComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('from tpl'); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + }); + + it('should create and remove embedded views', () => { + @Component({ + selector: 'app-multi', + template: `from tpl + + ` + }) + class NgContainerComponent { + showing = false; + } + + TestBed.configureTestingModule({declarations: [NgContainerComponent]}); + const fixture = TestBed.createComponent(NgContainerComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('from tpl'); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + }); + }); +}); diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts deleted file mode 100644 index 029c154027..0000000000 --- a/packages/core/test/render3/common_integration_spec.ts +++ /dev/null @@ -1,1094 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {NgForOfContext} from '@angular/common'; - -import {AttributeMarker, ɵɵdefineComponent, ɵɵelement, ɵɵgetCurrentView, ɵɵtemplateRefExtractor} from '../../src/render3/index'; -import {ɵɵbind, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolationV, ɵɵlistener, ɵɵload, ɵɵnextContext, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {ɵɵrestoreView} from '../../src/render3/state'; - -import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def'; -import {ComponentFixture, createDirective, getDirectiveOnNode} from './render_util'; - -describe('@angular/common integration', () => { - - describe('NgForOf', () => { - it('should update a loop', () => { - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵbind(item)); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - //
      - //
    • {{item}}
    • - //
    - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
    • first
    • second
    '); - - // change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    • first
    • second
    '); - - // remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
    • first
    '); - - // change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
    • one
    '); - - // add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html).toEqual('
    • one
    • two
    '); - }); - - it('should support ngForOf context variables', () => { - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵinterpolation3('', ctx.index, ' of ', ctx.count, ': ', item, '')); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - //
      - //
    • {{index}} of - // {{count}}: {{item}}
    • - //
    - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 3, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
    • 0 of 2: first
    • 1 of 2: second
    '); - - fixture.component.items.splice(1, 0, 'middle'); - fixture.update(); - expect(fixture.html) - .toEqual('
    • 0 of 3: first
    • 1 of 3: middle
    • 2 of 3: second
    '); - }); - - it('should instantiate directives inside directives properly in an ngFor', () => { - let dirs: any[] = []; - - const Dir = createDirective('dir'); - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dir', '']); - { ɵɵtext(1, 'comp text'); } - ɵɵelementEnd(); - // testing only - dirs.push(getDirectiveOnNode(0)); - } - }, - directives: [Dir] - }); - } - - function ngForTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - } - - /** */ - class MyApp { - rows: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, ngForTemplate, 1, 0, 'comp', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.rows)); - } - }, - directives: () => [NgForOf, Comp, Dir] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - '
    comp text
    comp text
    '); - expect(dirs.length).toBe(2); - expect(dirs[0] instanceof Dir).toBe(true); - expect(dirs[1] instanceof Dir).toBe(true); - - fixture.component.rows.push('third'); - fixture.update(); - expect(dirs.length).toBe(3); - expect(dirs[2] instanceof Dir).toBe(true); - expect(fixture.html) - .toEqual( - '
    comp text
    comp text
    comp text
    '); - }); - - it('should retain parent view listeners when the NgFor destroy views', () => { - - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵinterpolation1('', item, '')); - } - } - - class MyApp { - private _data: number[] = [1, 2, 3]; - items: number[] = []; - - toggle() { - if (this.items.length) { - this.items = []; - } else { - this.items = this._data; - } - } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 4, - vars: 1, - // - //
      - //
    • {{index}}
    • - //
    - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { - ɵɵlistener('click', function() { return ctx.toggle(); }); - ɵɵtext(1, 'Toggle List'); - } - ɵɵelementEnd(); - ɵɵelementStart(2, 'ul'); - { - ɵɵtemplate( - 3, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(3, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - const button = fixture.hostElement.querySelector('button') !; - - expect(fixture.html).toEqual('
      '); - - // this will fill the list - fixture.component.toggle(); - fixture.update(); - expect(fixture.html) - .toEqual('
      • 1
      • 2
      • 3
      '); - - button.click(); - fixture.update(); - - expect(fixture.html).toEqual('
        '); - - button.click(); - fixture.update(); - expect(fixture.html) - .toEqual('
        • 1
        • 2
        • 3
        '); - }); - - it('should support multiple levels of embedded templates', () => { - /** - *
          - *
        • - * {{cell}} - {{ row.value }} - {{ items.length }} - * - *
        • - *
        - */ - class MyApp { - items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function liTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { - ɵɵtemplate( - 1, spanTemplate, 2, 3, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row.data)); - } - } - - function spanTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit; - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext() as any; - ɵɵtextBinding( - 1, ɵɵinterpolation3('', cell, ' - ', row.value, ' - ', app.items.length, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
        • 1 - first - 22 - first - 2
        • 3 - second - 24 - second - 2
        '); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html) - .toEqual('
        • 1 - first - 12 - first - 1
        '); - - // Change an item - fixture.component.items[0].data[0] = 'one'; - fixture.update(); - expect(fixture.html) - .toEqual('
        • one - first - 12 - first - 1
        '); - - // Add an item - fixture.component.items[1] = {data: ['three', '4'], value: 'third'}; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        • one - first - 22 - first - 2
        • three - third - 24 - third - 2
        '); - }); - - it('should support multiple levels of embedded templates with listeners', () => { - /** - *
        - *

        - * - * {{ row.value }} - {{ name }} - *

        - *
        - */ - class MyApp { - items: any[] = [{data: ['1'], value: 'first'}]; - name = 'app'; - events: string[] = []; - - onClick(value: string, name: string) { this.events.push(value, name); } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, divTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function divTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtemplate(1, pTemplate, 3, 2, 'p', [AttributeMarker.Template, 'ngFor', 'ngForOf']); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row.data)); - } - } - - function pTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - const state = ɵɵgetCurrentView(); - ɵɵelementStart(0, 'p'); - { - ɵɵelementStart(1, 'span'); - { - ɵɵlistener('click', () => { - ɵɵrestoreView(state); - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext(); - app.onClick(row.value, app.name); - }); - } - ɵɵelementEnd(); - ɵɵtext(2); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext() as any; - ɵɵtextBinding(2, ɵɵinterpolation2('', row.value, ' - ', app.name, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - fixture.update(); - expect(fixture.html).toEqual('

        first - app

        '); - - const span = fixture.hostElement.querySelector('span') as any; - span.click(); - expect(fixture.component.events).toEqual(['first', 'app']); - - fixture.component.name = 'new name'; - fixture.update(); - expect(fixture.html).toEqual('

        first - new name

        '); - - span.click(); - expect(fixture.component.events).toEqual(['first', 'app', 'first', 'new name']); - }); - - it('should support skipping contexts', () => { - /** - *
        - *
        - * - * {{ cell.value }} - {{ name }} - * - *
        - *
        - */ - class MyApp { - name = 'app'; - items: any[] = [ - [ - // row - {value: 'one', data: ['1', '2']} // cell - ], - [{value: 'two', data: ['3', '4']}] - ]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, divTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function divTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, innerDivTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row)); - } - } - - function innerDivTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, spanTemplate, 2, 2, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(cell.data)); - } - } - - function spanTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext(2) as any; - ɵɵtextBinding(1, ɵɵinterpolation2('', cell.value, ' - ', app.name, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - fixture.update(); - expect(fixture.html) - .toEqual( - `
        one - appone - app
        two - apptwo - app
        `); - - fixture.component.name = 'other'; - fixture.update(); - expect(fixture.html) - .toEqual( - `
        one - otherone - other
        two - othertwo - other
        `); - }); - - it('should support context for 9+ levels of embedded templates', () => { - /** - * - * - * - * - * - * - * - * - * - * - * {{ item8 }} - {{ item7.value }} - {{ item6.value }}... - * - * - * - * - * - * - * - * - * - */ - class MyApp { - value = 'App'; - items: any[] = [ - { - // item0 - data: [{ - // item1 - data: [{ - // item2 - data: [{ - // item3 - data: [{ - // item4 - data: [{ - // item5 - data: [{ - // item6 - data: [{ - // item7 - data: [ - '1', '2' // item8 - ], - value: 'h' - }], - value: 'g' - }], - value: 'f' - }], - value: 'e' - }], - value: 'd' - }], - value: 'c' - }], - value: 'b' - }], - value: 'a' - }, - { - // item0 - data: [{ - // item1 - data: [{ - // item2 - data: [{ - // item3 - data: [{ - // item4 - data: [{ - // item5 - data: [{ - // item6 - data: [{ - // item7 - data: [ - '3', '4' // item8 - ], - value: 'H' - }], - value: 'G' - }], - value: 'F' - }], - value: 'E' - }], - value: 'D' - }], - value: 'C' - }], - value: 'B' - }], - value: 'A' - } - ]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, itemTemplate0, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function itemTemplate0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate1, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item0 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item0.data)); - } - } - - function itemTemplate1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate2, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item1 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item1.data)); - } - } - - function itemTemplate2(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate3, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item2 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item2.data)); - } - } - - function itemTemplate3(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate4, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item3 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item3.data)); - } - } - - function itemTemplate4(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate5, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item4 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item4.data)); - } - } - - function itemTemplate5(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate6, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item5 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item5.data)); - } - } - - function itemTemplate6(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate7, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item6 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item6.data)); - } - } - - function itemTemplate7(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate8, 2, 10, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item7 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item7.data)); - } - } - - function itemTemplate8(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - - if (rf & RenderFlags.Update) { - const value = ctx.$implicit; - const item7 = ɵɵnextContext().$implicit; - const item6 = ɵɵnextContext().$implicit; - const item5 = ɵɵnextContext().$implicit; - const item4 = ɵɵnextContext().$implicit; - const item3 = ɵɵnextContext().$implicit; - const item2 = ɵɵnextContext().$implicit; - const item1 = ɵɵnextContext().$implicit; - const item0 = ɵɵnextContext().$implicit; - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵinterpolationV([ - '', value, '.', item7.value, '.', item6.value, '.', item5.value, - '.', item4.value, '.', item3.value, '.', item2.value, '.', item1.value, - '.', item0.value, '.', myApp.value, '' - ])); - } - } - - const fixture = new ComponentFixture(MyApp); - - expect(fixture.html) - .toEqual( - '' + - '1.h.g.f.e.d.c.b.a.App' + - '2.h.g.f.e.d.c.b.a.App' + - '' + - '' + - '3.H.G.F.E.D.C.B.A.App' + - '4.H.G.F.E.D.C.B.A.App' + - ''); - }); - - }); - - describe('ngIf', () => { - it('should support sibling ngIfs', () => { - class MyApp { - showing = true; - valueOne = 'one'; - valueTwo = 'two'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 2, - /** - *
        {{ valueOne }}
        - *
        {{ valueTwo }}
        - */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, templateOne, 2, 1, 'div', [AttributeMarker.Template, 'ngIf']); - ɵɵtemplate(1, templateTwo, 2, 1, 'div', [AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - ɵɵelementProperty(1, 'ngIf', ɵɵbind(ctx.showing)); - } - - }, - directives: () => [NgIf] - }); - } - - function templateOne(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(myApp.valueOne)); - } - } - - function templateTwo(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(myApp.valueTwo)); - } - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
        one
        two
        '); - - fixture.component.valueOne = '$$one$$'; - fixture.component.valueTwo = '$$two$$'; - fixture.update(); - expect(fixture.html).toEqual('
        $$one$$
        $$two$$
        '); - }); - - it('should handle nested ngIfs with no intermediate context vars', () => { - /** - *
        - *
        - *
        - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, (rf1: RenderFlags) => { - if (rf1 & RenderFlags.Create) { - ɵɵtext(0, 'from tpl'); - } - }, 1, 0, 'ng-template', undefined, ['tpl', ''], ɵɵtemplateRefExtractor); - ɵɵtemplate( - 2, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'ngTemplateOutlet']); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵload(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.showing ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.showing = true; - fixture.update(); - expect(fixture.html).toEqual('from tpl'); - - fixture.component.showing = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should allow usage on ng-container', () => { - class MyApp { - showing = false; - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 3, - vars: 1, - /** - * from tpl - * - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, (rf1: RenderFlags) => { - if (rf1 & RenderFlags.Create) { - ɵɵtext(0, 'from tpl'); - } - }, 1, 0, 'ng-template', undefined, ['tpl', ''], ɵɵtemplateRefExtractor); - ɵɵelementContainerStart(2, [AttributeMarker.Bindings, 'ngTemplateOutlet']); - ɵɵelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.showing ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.showing = true; - fixture.update(); - expect(fixture.html).toEqual('from tpl'); - - fixture.component.showing = false; - fixture.update(); - expect(fixture.html).toEqual(''); - - }); - - }); -}); From f9fb921f911d203190f690e4230f043f0b722686 Mon Sep 17 00:00:00 2001 From: Judy Bogart Date: Thu, 9 May 2019 08:21:06 -0700 Subject: [PATCH 025/275] docs: update ivy preview page (#30355) PR Close #30355 --- aio/content/guide/ivy.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aio/content/guide/ivy.md b/aio/content/guide/ivy.md index e62baa7ee7..28c5a7ab7f 100644 --- a/aio/content/guide/ivy.md +++ b/aio/content/guide/ivy.md @@ -1,6 +1,12 @@ # Opting into Angular Ivy -Ivy is the code name for Angular's [next-generation compilation and rendering pipeline](https://blog.angular.io/a-plan-for-version-8-0-and-ivy-b3318dfc19f7). Starting with Angular version 8, you can choose to opt in to start using Ivy now, and help in its continuing develpment and tuning. +Ivy is the code name for Angular's [next-generation compilation and rendering pipeline](https://blog.angular.io/a-plan-for-version-8-0-and-ivy-b3318dfc19f7). Starting with Angular version 8, you can choose to opt in to start using a preview version of Ivy and help in its continuing development and tuning. + +
        + + To preview Ivy, use `@angular/core@next` version of Angular (8.1.x), rather than `@angular/core@latest` (8.0.x), as it contains all the latest bug fixes and improvements. + +
        ## Using Ivy in a new project From 6ceb9034dc99e5e9f791c988eda2cc9ce99bc2e1 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 14 May 2019 19:59:26 +0200 Subject: [PATCH 026/275] fix(core): static-query migration errors not printed properly (#30458) Apparently the devkit logger is not able to properly print out error objects, so we need to convert them to a string before in order to make the error visible to the user. This is not testable without an e2e test that validates the CLI terminal output. PR Close #30458 --- packages/core/schematics/migrations/static-queries/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts index 089d369d7c..efc8a5f525 100644 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ b/packages/core/schematics/migrations/static-queries/index.ts @@ -192,7 +192,7 @@ async function runStaticQueryMigration( `the update cannot use the template migration strategy. Please ensure ` + `there are no AOT compilation errors.`); } - logger.error(e); + logger.error(e.toString()); logger.info( 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"'); return []; From 0cdf5980e299d70434f410421966be403f7f09c3 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 14 May 2019 20:35:27 +0200 Subject: [PATCH 027/275] fix(core): static-query migration should not fallback to test strategy (#30458) Currently if something fails in the selected strategy (e.g. AOT failures), the migration currently accidentally falls back to the test strategy. This is not helpful as we want to give developers the possibility to re-run the migration after fixing potential AOT failures. PR Close #30458 --- .../migrations/static-queries/index.ts | 19 +++++++-- .../static_queries_migration_template_spec.ts | 42 ++++++++++++++++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts index efc8a5f525..19bdba3934 100644 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ b/packages/core/schematics/migrations/static-queries/index.ts @@ -61,11 +61,12 @@ async function runMigration(tree: Tree, context: SchematicContext) { 'to explicit timing.'); } + const analyzedFiles = new Set(); const buildProjects = new Set(); const failures = []; for (const tsconfigPath of buildPaths) { - const project = analyzeProject(tree, tsconfigPath, basePath); + const project = analyzeProject(tree, tsconfigPath, basePath, analyzedFiles); if (project) { buildProjects.add(project); } @@ -83,7 +84,7 @@ async function runMigration(tree: Tree, context: SchematicContext) { // For the "test" tsconfig projects we always want to use the test strategy as // we can't detect the proper timing within spec files. for (const tsconfigPath of testPaths) { - const project = await analyzeProject(tree, tsconfigPath, basePath); + const project = await analyzeProject(tree, tsconfigPath, basePath, analyzedFiles); if (project) { failures.push( ...await runStaticQueryMigration(tree, project, SELECTED_STRATEGY.TESTS, logger)); @@ -103,7 +104,8 @@ async function runMigration(tree: Tree, context: SchematicContext) { * Analyzes the given TypeScript project by looking for queries that need to be * migrated. In case there are no queries that can be migrated, null is returned. */ -function analyzeProject(tree: Tree, tsconfigPath: string, basePath: string): +function analyzeProject( + tree: Tree, tsconfigPath: string, basePath: string, analyzedFiles: Set): AnalyzedProject|null { const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath)); const host = ts.createCompilerHost(parsed.options, true); @@ -125,7 +127,16 @@ function analyzeProject(tree: Tree, tsconfigPath: string, basePath: string): // Analyze all project source-files and collect all queries that // need to be migrated. - sourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile)); + sourceFiles.forEach(sourceFile => { + const relativePath = relative(basePath, sourceFile.fileName); + + // Only look for queries within the current source files if the + // file has not been analyzed before. + if (!analyzedFiles.has(relativePath)) { + analyzedFiles.add(relativePath); + queryVisitor.visitNode(sourceFile); + } + }); if (queryVisitor.resolvedQueries.size === 0) { return null; diff --git a/packages/core/schematics/test/static_queries_migration_template_spec.ts b/packages/core/schematics/test/static_queries_migration_template_spec.ts index e8f6b96815..31483df180 100644 --- a/packages/core/schematics/test/static_queries_migration_template_spec.ts +++ b/packages/core/schematics/test/static_queries_migration_template_spec.ts @@ -515,8 +515,6 @@ describe('static-queries migration with template strategy', () => { await runMigration(); expect(errorOutput.length).toBe(1); - expect(errorOutput[0]) - .toMatch(/^Error: Could not create Angular AOT compiler to determine query timing./); expect(errorOutput[0]).toMatch(/Cannot determine the module for class MyComp/); }); @@ -673,15 +671,49 @@ describe('static-queries migration with template strategy', () => { export class MyModule {} `); - spyOn(console, 'error').and.callThrough(); - await runMigration(); - expect(console.error).toHaveBeenCalledTimes(0); + expect(errorOutput.length).toBe(0); expect(tree.readContent('/src/test.ts')) .toContain(`@ViewChild('test', /* TODO: add static flag */ {}) query: any;`); expect(tree.readContent('/src/app.component.ts')) .toContain(`@ViewChild('test', { static: true }) query: any;`); }); + + it('should not fall back to test strategy if selected strategy fails', async() => { + writeFile('/src/tsconfig.spec.json', JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + lib: ['es2015'], + }, + files: [ + 'test.ts', + ], + })); + + writeFile('/src/test.ts', `import * as mod from './app.module';`); + writeFile('/src/app.component.ts', ` + import {Component, ViewChild} from '@angular/core'; + + @Component({template: 'Test'}) + export class AppComponent { + @ViewChild('test') query: any; + } + `); + + writeFile('/src/app.module.ts', ` + import {NgModule} from '@angular/core'; + import {AppComponent} from './app.component'; + + @NgModule({declarations: [AppComponent, ThisCausesAnError]}) + export class MyModule {} + `); + + await runMigration(); + + expect(errorOutput.length).toBe(1); + expect(errorOutput[0]).toMatch(/Unexpected value 'undefined'/); + expect(tree.readContent('/src/app.component.ts')).toContain(`@ViewChild('test') query: any;`); + }); }); }); From 9e5377a2e3c4431f73985bc9272f5bf09dc6e30d Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 14 May 2019 20:59:21 +0200 Subject: [PATCH 028/275] refactor(core): improve messages for static-query migrations (#30458) Slightly improves the messages for the static-query migration in order to make the terminal output less verbose but more helpful. Unfortunately we are limited in what we can print due to the devkit not providing much utilities for printing good messages from a migration schematic. PR Close #30458 --- .../migrations/static-queries/index.ts | 22 +++++++++---------- .../template_strategy/template_strategy.ts | 6 +---- .../migrations/static-queries/transform.ts | 2 +- .../static_queries_migration_template_spec.ts | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts index 19bdba3934..505c404bde 100644 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ b/packages/core/schematics/migrations/static-queries/index.ts @@ -53,7 +53,6 @@ async function runMigration(tree: Tree, context: SchematicContext) { logger.info('In preparation for Ivy, developers can now explicitly specify the'); logger.info('timing of their queries. Read more about this here:'); logger.info('https://github.com/angular/angular/pull/28810'); - logger.info(''); if (!buildPaths.length && !testPaths.length) { throw new SchematicsException( @@ -92,6 +91,7 @@ async function runMigration(tree: Tree, context: SchematicContext) { } if (failures.length) { + logger.info(''); logger.info('Some queries could not be migrated automatically. Please go'); logger.info('through those manually and apply the appropriate timing:'); failures.forEach(failure => logger.warn(`⮑ ${failure}`)); @@ -153,7 +153,7 @@ function analyzeProject( */ async function runStaticQueryMigration( tree: Tree, project: AnalyzedProject, selectedStrategy: SELECTED_STRATEGY, - logger: logging.LoggerApi) { + logger: logging.LoggerApi): Promise { const {sourceFiles, typeChecker, host, queryVisitor, tsconfigPath, basePath} = project; const printer = ts.createPrinter(); const failureMessages: string[] = []; @@ -190,22 +190,22 @@ async function runStaticQueryMigration( try { strategy.setup(); } catch (e) { - // In case the strategy could not be set up properly, we just exit the - // migration. We don't want to throw an exception as this could mean - // that other migrations are interrupted. - logger.warn( - `Could not setup migration strategy for "${project.tsconfigPath}". The ` + - `following error has been reported:`); if (selectedStrategy === SELECTED_STRATEGY.TEMPLATE) { logger.warn( `The template migration strategy uses the Angular compiler ` + `internally and therefore projects that no longer build successfully after ` + `the update cannot use the template migration strategy. Please ensure ` + - `there are no AOT compilation errors.`); + `there are no AOT compilation errors.\n`); } - logger.error(e.toString()); + // In case the strategy could not be set up properly, we just exit the + // migration. We don't want to throw an exception as this could mean + // that other migrations are interrupted. + logger.warn( + `Could not setup migration strategy for "${project.tsconfigPath}". The ` + + `following error has been reported:\n`); + logger.error(`${e.toString()}\n`); logger.info( - 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"'); + 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n'); return []; } diff --git a/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts b/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts index f7c0084470..6157ab3b99 100644 --- a/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts +++ b/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts @@ -179,11 +179,7 @@ export class QueryTemplateStrategy implements TimingStrategy { } private _createDiagnosticsError(diagnostics: (ts.Diagnostic|Diagnostic)[]) { - return new Error( - `Could not create Angular AOT compiler to determine query timing.\n` + - `The following diagnostics were detected:\n` + - `${diagnostics.map(d => d.messageText).join(`\n `)}\n` + - `Please make sure that there is no AOT compilation failure.`); + return new Error(`${diagnostics.map(d => d.messageText).join(`\n `)}`); } private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) { diff --git a/packages/core/schematics/migrations/static-queries/transform.ts b/packages/core/schematics/migrations/static-queries/transform.ts index 139df0afa2..f60875799a 100644 --- a/packages/core/schematics/migrations/static-queries/transform.ts +++ b/packages/core/schematics/migrations/static-queries/transform.ts @@ -72,7 +72,7 @@ export function getTransformedQueryCallExpr( // we create a transformation failure message that shows developers that they need // to set the query timing manually to the determined query timing. if (timing !== null) { - failureMessage = 'Cannot update query declaration to explicit timing. Please manually ' + + failureMessage = 'Cannot update query to set explicit timing. Please manually ' + `set the query timing to: "{static: ${(timing === QueryTiming.STATIC).toString()}}"`; } } diff --git a/packages/core/schematics/test/static_queries_migration_template_spec.ts b/packages/core/schematics/test/static_queries_migration_template_spec.ts index 31483df180..4376b2d97f 100644 --- a/packages/core/schematics/test/static_queries_migration_template_spec.ts +++ b/packages/core/schematics/test/static_queries_migration_template_spec.ts @@ -602,7 +602,7 @@ describe('static-queries migration with template strategy', () => { .toContain(`@ViewChild('myRef', /* TODO: add static flag */ myOptionsVar) query: any;`); expect(warnOutput.length).toBe(1); expect(warnOutput[0]) - .toMatch(/^⮑ {3}index.ts@8:11: Cannot update query declaration to explicit timing./); + .toMatch(/^⮑ {3}index.ts@8:11: Cannot update query to set explicit timing./); expect(warnOutput[0]).toMatch(/Please manually set the query timing to.*static: true/); }); From dbb150a9bdae68dfb92d64d904e85b8c5db5b2ec Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 14 May 2019 14:06:01 -0700 Subject: [PATCH 029/275] Revert "fix(core): CSS sanitizer now allows parens in file names (#30322)" (#30463) This reverts commit 728db882808869e1f52d20535676756d3b63b58a. We're reverting this commit for now, until it can be subjected to a more thorough security review. PR Close #30463 --- packages/core/src/sanitization/style_sanitizer.ts | 2 +- packages/core/test/sanitization/style_sanitizer_spec.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/sanitization/style_sanitizer.ts b/packages/core/src/sanitization/style_sanitizer.ts index 16b19de093..62a6ecbe59 100644 --- a/packages/core/src/sanitization/style_sanitizer.ts +++ b/packages/core/src/sanitization/style_sanitizer.ts @@ -54,7 +54,7 @@ const SAFE_STYLE_VALUE = new RegExp( * Given the common use case, low likelihood of attack vector, and low impact of an attack, this * code is permissive and allows URLs that sanitize otherwise. */ -const URL_RE = /^url\(([\w\W]*)\)$/; +const URL_RE = /^url\(([^)]+)\)$/; /** * Checks that quotes (" and ') are properly balanced inside a string. Assumes diff --git a/packages/core/test/sanitization/style_sanitizer_spec.ts b/packages/core/test/sanitization/style_sanitizer_spec.ts index e5a9d200e6..5adafceb8e 100644 --- a/packages/core/test/sanitization/style_sanitizer_spec.ts +++ b/packages/core/test/sanitization/style_sanitizer_spec.ts @@ -32,7 +32,7 @@ import {_sanitizeStyle} from '../../src/sanitization/style_sanitizer'; expectSanitize('rgb(255, 0, 0)').toEqual('rgb(255, 0, 0)'); expectSanitize('expression(haha)').toEqual('unsafe'); }); - t.it('rejects unbalanced quotes', () => { expectSanitize('"value" "').toEqual('unsafe'); }); + t.it('rejects unblanaced quotes', () => { expectSanitize('"value" "').toEqual('unsafe'); }); t.it('accepts transform functions', () => { expectSanitize('rotate(90deg)').toEqual('rotate(90deg)'); expectSanitize('rotate(javascript:evil())').toEqual('unsafe'); @@ -58,7 +58,6 @@ import {_sanitizeStyle} from '../../src/sanitization/style_sanitizer'; t.it('accepts quoted URLs', () => { expectSanitize('url("foo/bar.png")').toEqual('url("foo/bar.png")'); expectSanitize(`url('foo/bar.png')`).toEqual(`url('foo/bar.png')`); - expectSanitize(`url('foo/bar (1).png')`).toEqual(`url('foo/bar (1).png')`); expectSanitize(`url( 'foo/bar.png'\n )`).toEqual(`url( 'foo/bar.png'\n )`); expectSanitize('url("javascript:evil()")').toEqual('unsafe'); expectSanitize('url( " javascript:evil() " )').toEqual('unsafe'); From cf86ed7b29440aa23ed710542a41ffd09563266d Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Thu, 9 May 2019 11:47:25 -0700 Subject: [PATCH 030/275] =?UTF-8?q?refactor(ivy):=20migrate=20=C9=B5=C9=B5?= =?UTF-8?q?=20prefix=20back=20to=20=CE=94=20(#30362)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that issues are resolved with Closure compiler, we can move back to our desired prefix of `Δ`. PR Close #30362 --- aio/content/guide/deprecations.md | 94 +- .../transforms/angular-api-package/index.js | 2 +- docs/PUBLIC_API.md | 2 +- integration/ngcc/test.sh | 14 +- .../src/tree/render3_function/index.ts | 46 +- packages/common/src/directives/ng_class.ts | 10 +- packages/common/src/directives/ng_style.ts | 10 +- packages/common/src/viewport_scroller.ts | 6 +- .../ngcc/test/rendering/renderer_spec.ts | 12 +- packages/compiler-cli/src/metadata/bundler.ts | 2 +- .../src/ngtsc/imports/src/core.ts | 16 +- .../test/compliance/mock_compile.ts | 2 +- .../test/compliance/mock_compiler_spec.ts | 2 +- .../compliance/r3_compiler_compliance_spec.ts | 762 +++++----- .../r3_view_compiler_binding_spec.ts | 146 +- .../compliance/r3_view_compiler_di_spec.ts | 14 +- .../r3_view_compiler_directives_spec.ts | 56 +- .../compliance/r3_view_compiler_i18n_spec.ts | 1353 +++++++++-------- .../r3_view_compiler_input_outputs_spec.ts | 4 +- .../r3_view_compiler_listener_spec.ts | 62 +- .../r3_view_compiler_providers_spec.ts | 6 +- .../test/compliance/r3_view_compiler_spec.ts | 16 +- .../r3_view_compiler_styling_spec.ts | 388 ++--- .../r3_view_compiler_template_spec.ts | 192 +-- packages/compiler-cli/test/ngc_spec.ts | 12 +- .../test/ngtsc/fake_core/index.ts | 6 +- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 322 ++-- .../compiler-cli/test/ngtsc/scope_spec.ts | 12 +- .../test/ngtsc/template_mapping_spec.ts | 164 +- .../test/ngtsc/template_typecheck_spec.ts | 6 +- packages/compiler/design/architecture.md | 10 +- .../compiler/design/separate_compilation.md | 14 +- packages/compiler/src/identifiers.ts | 6 +- packages/compiler/src/injectable_compiler.ts | 2 +- .../compiler/src/injectable_compiler_2.ts | 2 +- .../compiler/src/render3/r3_identifiers.ts | 240 +-- .../src/render3/r3_module_compiler.ts | 6 +- .../compiler/src/render3/view/template.ts | 2 +- .../differs/iterable_differs.ts | 4 +- .../differs/keyvalue_differs.ts | 4 +- packages/core/src/core_private_export.ts | 4 +- .../core/src/core_render3_private_export.ts | 242 +-- packages/core/src/di/index.ts | 4 +- packages/core/src/di/injectable.ts | 6 +- packages/core/src/di/injection_token.ts | 4 +- packages/core/src/di/injector.ts | 8 +- .../core/src/di/injector_compatibility.ts | 17 +- packages/core/src/di/interface/defs.ts | 20 +- packages/core/src/di/jit/environment.ts | 12 +- packages/core/src/di/r3_injector.ts | 8 +- packages/core/src/di/util.ts | 4 +- packages/core/src/metadata/ng_module.ts | 8 +- packages/core/src/r3_symbols.ts | 8 +- packages/core/src/render3/CODE_GEN_API.md | 8 +- packages/core/src/render3/STATUS.md | 32 +- .../src/render3/STORING_METADATA_IN_D.TS.md | 6 +- packages/core/src/render3/VIEW_DATA.md | 42 +- packages/core/src/render3/component_ref.ts | 4 +- packages/core/src/render3/definition.ts | 28 +- packages/core/src/render3/di.ts | 6 +- packages/core/src/render3/di_setup.ts | 6 +- .../features/inherit_definition_feature.ts | 6 +- .../render3/features/ng_onchanges_feature.ts | 2 +- .../src/render3/features/providers_feature.ts | 2 +- packages/core/src/render3/i18n.ts | 32 +- packages/core/src/render3/index.ts | 230 +-- .../render3/instructions/alloc_host_vars.ts | 2 +- .../src/render3/instructions/container.ts | 8 +- packages/core/src/render3/instructions/di.ts | 16 +- .../core/src/render3/instructions/element.ts | 14 +- .../render3/instructions/element_container.ts | 4 +- .../src/render3/instructions/embedded_view.ts | 5 +- .../render3/instructions/get_current_view.ts | 2 +- .../core/src/render3/instructions/listener.ts | 4 +- .../src/render3/instructions/namespace.ts | 2 +- .../src/render3/instructions/next_context.ts | 2 +- .../src/render3/instructions/projection.ts | 4 +- .../core/src/render3/instructions/property.ts | 12 +- .../instructions/property_interpolation.ts | 98 +- .../core/src/render3/instructions/select.ts | 4 +- .../core/src/render3/instructions/shared.ts | 4 +- .../core/src/render3/instructions/storage.ts | 4 +- .../core/src/render3/instructions/styling.ts | 12 +- .../core/src/render3/instructions/text.ts | 4 +- .../core/src/render3/interfaces/definition.ts | 10 +- packages/core/src/render3/jit/environment.ts | 242 +-- packages/core/src/render3/pipe.ts | 39 +- packages/core/src/render3/pure_function.ts | 20 +- packages/core/src/render3/query.ts | 22 +- packages/core/src/render3/state.ts | 20 +- packages/core/src/render3/util/misc_utils.ts | 6 +- .../view_engine_compatibility_prebound.ts | 2 +- .../core/src/sanitization/sanitization.ts | 20 +- packages/core/src/view/ng_module.ts | 6 +- packages/core/src/view/services.ts | 4 +- .../cyclic_import/bundle.golden_symbols.json | 20 +- .../hello_world/bundle.golden_symbols.json | 8 +- .../injection/bundle.golden_symbols.json | 6 +- .../core/test/bundling/injection/usage.ts | 8 +- .../bundling/todo/bundle.golden_symbols.json | 46 +- .../core/test/bundling/todo_i18n/index.ts | 2 +- packages/core/test/di/r3_injector_spec.ts | 60 +- .../test/linker/ng_module_integration_spec.ts | 6 +- packages/core/test/render3/basic_perf.ts | 22 +- .../test/render3/change_detection_spec.ts | 62 +- packages/core/test/render3/common_with_def.ts | 18 +- .../core/test/render3/component_ref_spec.ts | 8 +- packages/core/test/render3/component_spec.ts | 177 ++- packages/core/test/render3/content_spec.ts | 734 ++++----- .../core/test/render3/control_flow_spec.ts | 450 +++--- packages/core/test/render3/debug_spec.ts | 10 +- packages/core/test/render3/di_spec.ts | 180 +-- packages/core/test/render3/exports_spec.ts | 24 +- packages/core/test/render3/i18n_spec.ts | 40 +- .../inherit_definition_feature_spec.ts | 128 +- .../core/test/render3/instructions_spec.ts | 168 +- .../core/test/render3/integration_spec.ts | 1005 ++++++------ packages/core/test/render3/ivy/jit_spec.ts | 28 +- .../core/test/render3/jit_environment_spec.ts | 12 +- packages/core/test/render3/lifecycle_spec.ts | 1028 +++++++------ packages/core/test/render3/listeners_spec.ts | 300 ++-- packages/core/test/render3/outputs_spec.ts | 42 +- packages/core/test/render3/pipe_spec.ts | 16 +- packages/core/test/render3/providers_spec.ts | 459 +++--- .../core/test/render3/pure_function_spec.ts | 28 +- packages/core/test/render3/query_spec.ts | 728 +++++---- packages/core/test/render3/render_util.ts | 8 +- .../test/render3/renderer_factory_spec.ts | 60 +- .../styling/class_and_style_bindings_spec.ts | 124 +- .../core/test/render3/styling/players_spec.ts | 34 +- .../core/test/render3/template_ref_spec.ts | 34 +- .../test/render3/view_container_ref_spec.ts | 530 +++---- .../test/sanitization/sanatization_spec.ts | 77 +- .../test/strict_types/inheritance_spec.ts | 6 +- packages/core/test/test_bed_spec.ts | 2 +- packages/core/test/view/ng_module_spec.ts | 20 +- .../core/testing/src/r3_test_bed_compiler.ts | 2 +- packages/core/testing/src/test_bed.ts | 4 +- packages/platform-browser/src/browser/meta.ts | 4 +- .../platform-browser/src/browser/title.ts | 4 +- tools/public_api_guard/core/core.d.ts | 764 +++++----- tools/ts-api-guardian/index.bzl | 4 +- 142 files changed, 6417 insertions(+), 6453 deletions(-) diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index 7903bc9947..023455cdce 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -1,11 +1,11 @@ # Deprecated APIs and Features -Angular strives to balance innovation and stability. -Sometimes, APIs and features become obsolete and need to be removed or replaced so that Angular can stay current with new best practices, changing dependencies, or changes in the (web) platform itself. +Angular strives to balance innovation and stability. +Sometimes, APIs and features become obsolete and need to be removed or replaced so that Angular can stay current with new best practices, changing dependencies, or changes in the (web) platform itself. To make these transitions as easy as possible, we deprecate APIs and features for a period of time before removing them. This gives you time to update your apps to the latest APIs and best practices. -This guide contains a summary of all Angular APIs and features that are currently deprecated. +This guide contains a summary of all Angular APIs and features that are currently deprecated.
        @@ -13,7 +13,7 @@ This guide contains a summary of all Angular APIs and features that are currentl Features and APIs that were deprecated in v6 or earlier are candidates for removal in version 9 or any later major version. For information about Angular's deprecation and removal practices, see [Angular Release Practices](guide/releases#deprecation-practices "Angular Release Practices: Deprecation practices"). -For step-by-step instructions on how to update to the latest Angular release, use the interactive update guide at [update.angular.io](https://update.angular.io). +For step-by-step instructions on how to update to the latest Angular release, use the interactive update guide at [update.angular.io](https://update.angular.io).
        @@ -21,7 +21,7 @@ For step-by-step instructions on how to update to the latest Angular release, us ## Deprecated APIs -This section contains a complete list all of the currently-deprecated APIs, with details to help you plan your migration to a replacement. +This section contains a complete list all of the currently-deprecated APIs, with details to help you plan your migration to a replacement.
        @@ -49,16 +49,16 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i | API | Replacement | Deprecation announced | Notes | | --- | ----------- | --------------------- | ----- | -| [`CollectionChangeRecord`](api/core/CollectionChangeRecord) | [`IterableChangeRecord`](api/core/IterableChangeRecord) | v4 | none | +| [`CollectionChangeRecord`](api/core/CollectionChangeRecord) | [`IterableChangeRecord`](api/core/IterableChangeRecord) | v4 | none | | [`DefaultIterableDiffer`](api/core/DefaultIterableDiffer) | n/a | v4 | Not part of public API. | -| [`defineInjectable`](api/core/defineInjectable) | `ɵɵdefineInjectable` | v8 | Used only in generated code. No source code should depend on this API. | -| [`inject`](api/core/inject) | `ɵɵinject` | v8 | Used only in generated code. No source code should depend on this API. | -| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) | +| [`defineInjectable`](api/core/defineInjectable) | `ΔdefineInjectable` | v8 | Used only in generated code. No source code should depend on this API. | +| [`inject`](api/core/inject) | `Δinject` | v8 | Used only in generated code. No source code should depend on this API. | +| [`ReflectiveInjector`](api/core/ReflectiveInjector) | [`Injector.create`](api/core/Injector#create) | v5 | See [`ReflectiveInjector`](#reflectiveinjector) | | [`ReflectiveKey`](api/core/ReflectiveKey) | none | v5 | none | -| [`RenderComponentType`](api/core/RenderComponentType) | [`RendererType2`](api/core/RendererType2) and [`Renderer2`](api/core/Renderer2) | v4 | none | -| [`Renderer`](api/core/Renderer) | [`Renderer2`](api/core/Renderer2) | v4 | none | -| [`RootRenderer`](api/core/RootRenderer) | [`RendererFactory2`](api/core/RendererFactory2) | v4 | none | -| [`ViewEncapsulation.Native`](api/core/ViewEncapsulation#Native) | [`ViewEncapsulation.ShadowDom`](api/core/ViewEncapsulation#ShadowDom) | v6 | Use the native encapsulation mechanism of the renderer. See [view.ts](https://github.com/angular/angular/blob/3e992e18ebf51d6036818f26c3d77b52d3ec48eb/packages/core/src/metadata/view.ts#L32). +| [`RenderComponentType`](api/core/RenderComponentType) | [`RendererType2`](api/core/RendererType2) and [`Renderer2`](api/core/Renderer2) | v4 | none | +| [`Renderer`](api/core/Renderer) | [`Renderer2`](api/core/Renderer2) | v4 | none | +| [`RootRenderer`](api/core/RootRenderer) | [`RendererFactory2`](api/core/RendererFactory2) | v4 | none | +| [`ViewEncapsulation.Native`](api/core/ViewEncapsulation#Native) | [`ViewEncapsulation.ShadowDom`](api/core/ViewEncapsulation#ShadowDom) | v6 | Use the native encapsulation mechanism of the renderer. See [view.ts](https://github.com/angular/angular/blob/3e992e18ebf51d6036818f26c3d77b52d3ec48eb/packages/core/src/metadata/view.ts#L32). #### @angular/core/testing @@ -72,7 +72,7 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i | API | Replacement | Deprecation announced | Notes | | --- | ----------- | --------------------- | ----- | -| [`NgFormSelectorWarning`](api/forms/NgFormSelectorWarning) | n/a | v6 | See [ngForm](#ngform). | +| [`NgFormSelectorWarning`](api/forms/NgFormSelectorWarning) | n/a | v6 | See [ngForm](#ngform). | #### @angular/router @@ -93,15 +93,15 @@ Tip: In the [API reference section](api) of this doc site, deprecated APIs are i | API | Replacement | Deprecation announced | Notes | | --- | ----------- | --------------------- | ----- | -| [`getAngularLib`](api/upgrade/static/getAngularLib) | [`getAngularJSGlobal`](api/upgrade/static/getAngularJSGlobal) | v5 | See [Upgrading from AngularJS](guide/upgrade). | -[`setAngularLib`](api/upgrade/static/setAngularLib) | [`setAngularJSGlobal`](api/upgrade/static/setAngularJSGlobal) | v5 | See [Upgrading from AngularJS](guide/upgrade). | +| [`getAngularLib`](api/upgrade/static/getAngularLib) | [`getAngularJSGlobal`](api/upgrade/static/getAngularJSGlobal) | v5 | See [Upgrading from AngularJS](guide/upgrade). | +[`setAngularLib`](api/upgrade/static/setAngularLib) | [`setAngularJSGlobal`](api/upgrade/static/setAngularJSGlobal) | v5 | See [Upgrading from AngularJS](guide/upgrade). | {@a deprecated-features} ## Deprecated features -This section lists all of the currently-deprecated features, which includes template syntax, configuration options, and any other deprecations not listed in the [Deprecated APIs](#deprecated-apis) section above. It also includes deprecated API usage scenarios or API combinations, to augment the information above. +This section lists all of the currently-deprecated features, which includes template syntax, configuration options, and any other deprecations not listed in the [Deprecated APIs](#deprecated-apis) section above. It also includes deprecated API usage scenarios or API combinations, to augment the information above. @@ -111,20 +111,20 @@ This section lists all of the currently-deprecated features, which includes temp The shadow-piercing descendant combinator is deprecated and support is being removed from major browsers and tools. As such, in v4 we deprecated support in Angular for all 3 of `/deep/`, `>>>` and `::ng-deep`. Until removal, `::ng-deep` is preferred for broader compatibility with the tools. For more information, see [/deep/, >>>, and ::ng-deep](guide/component-styles#deprecated-deep--and-ng-deep "Component Styles guide, Deprecated deep and ngdeep") - in the Component Styles guide. + in the Component Styles guide. {@a template-tag} ### <template> tag -The `