/**
* @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 = `
`;
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 = `
`;
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 = `
`;
@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 1Section 2Section 3`);
}
});
});