From 7f17c70fd06eff5a3e487dccd88ec19996b2b600 Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Mon, 3 Dec 2018 16:34:25 +0100 Subject: [PATCH] test(ivy): add bundling test `todo_i18n` (#27420) PR Close #27420 --- packages/core/src/render3/index.ts | 3 +- packages/core/test/bundling/todo/index.ts | 3 +- .../core/test/bundling/todo/todo_e2e_spec.ts | 4 +- .../core/test/bundling/todo_i18n/BUILD.bazel | 115 ++++++ .../bundling/todo_i18n/OUTSTANDING_WORK.md | 36 ++ .../core/test/bundling/todo_i18n/base.css | 141 +++++++ .../core/test/bundling/todo_i18n/index.html | 57 +++ .../core/test/bundling/todo_i18n/index.ts | 181 +++++++++ .../core/test/bundling/todo_i18n/todo.css | 378 ++++++++++++++++++ .../test/bundling/todo_i18n/todo_e2e_spec.ts | 36 ++ .../test/bundling/todo_i18n/translations.ts | 42 ++ packages/goog.d.ts | 1 + packages/private/testing/src/goog_get_msg.ts | 5 +- 13 files changed, 996 insertions(+), 6 deletions(-) create mode 100644 packages/core/test/bundling/todo_i18n/BUILD.bazel create mode 100644 packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md create mode 100644 packages/core/test/bundling/todo_i18n/base.css create mode 100644 packages/core/test/bundling/todo_i18n/index.html create mode 100644 packages/core/test/bundling/todo_i18n/index.ts create mode 100644 packages/core/test/bundling/todo_i18n/todo.css create mode 100644 packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts create mode 100644 packages/core/test/bundling/todo_i18n/translations.ts diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index daa8221a91..9128b9901d 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -7,7 +7,7 @@ */ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'; import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; -import {getHostElement, getRenderedText} from './discovery_utils'; +import {getComponent, getHostElement, getRenderedText} from './discovery_utils'; import {InheritDefinitionFeature} from './features/inherit_definition_feature'; import {NgOnChangesFeature} from './features/ng_onchanges_feature'; import {ProvidersFeature} from './features/providers_feature'; @@ -169,6 +169,7 @@ export { defineBase, definePipe, getHostElement, + getComponent, getRenderedText, renderComponent, whenRendered, diff --git a/packages/core/test/bundling/todo/index.ts b/packages/core/test/bundling/todo/index.ts index eef6e234ae..a16c76ecd3 100644 --- a/packages/core/test/bundling/todo/index.ts +++ b/packages/core/test/bundling/todo/index.ts @@ -179,5 +179,4 @@ class ToDoAppComponent { class ToDoAppModule { } -// TODO(misko): create cleaner way to publish component into global location for tests. -(window as any).toDoAppComponent = renderComponent(ToDoAppComponent); +renderComponent(ToDoAppComponent); diff --git a/packages/core/test/bundling/todo/todo_e2e_spec.ts b/packages/core/test/bundling/todo/todo_e2e_spec.ts index 924210b50e..2beca5a55e 100644 --- a/packages/core/test/bundling/todo/todo_e2e_spec.ts +++ b/packages/core/test/bundling/todo/todo_e2e_spec.ts @@ -8,6 +8,7 @@ import '@angular/compiler'; import {ɵwhenRendered as whenRendered} from '@angular/core'; +import {getComponent} from '@angular/core/src/render3'; import {withBody} from '@angular/private/testing'; import * as path from 'path'; @@ -22,8 +23,7 @@ describe('functional test for todo', () => { describe(bundle, () => { it('should render todo', withBody('', async() => { require(path.join(PACKAGE, bundle)); - // TODO(misko): have cleaner way to do this for tests. - const toDoAppComponent = (window as any).toDoAppComponent; + const toDoAppComponent = getComponent(document.querySelector('todo-app') !); expect(document.body.textContent).toContain('todos'); expect(document.body.textContent).toContain('Demonstrate Components'); expect(document.body.textContent).toContain('4 items left'); diff --git a/packages/core/test/bundling/todo_i18n/BUILD.bazel b/packages/core/test/bundling/todo_i18n/BUILD.bazel new file mode 100644 index 0000000000..d2dea295a8 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/BUILD.bazel @@ -0,0 +1,115 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "jasmine_node_test", "ng_module", "ng_rollup_bundle", "ts_library") +load("//tools/http-server:http_server.bzl", "http_server") +load("@build_bazel_rules_typescript//:defs.bzl", "ts_devserver") + +ng_module( + name = "todo_i18n", + srcs = [ + "index.ts", + "translations.ts", + ], + tags = [ + "ivy-only", + ], + deps = [ + "//packages/common", + "//packages/core", + "//packages/core/test/bundling/util:reflect_metadata", + ], +) + +ng_rollup_bundle( + name = "bundle", + # TODO(alexeagle): This is inconsistent. + # We try to teach users to always have their workspace at the start of a + # path, to disambiguate from other workspaces. + # Here, the rule implementation is looking in an execroot where the layout + # has an "external" directory for external dependencies. + # This should probably start with "angular/" and let the rule deal with it. + entry_point = "packages/core/test/bundling/todo_i18n/index.js", + tags = [ + "ivy-only", + ], + deps = [ + ":todo_i18n", + "//packages/common", + "//packages/core", + "//packages/core/test/bundling/util:reflect_metadata", + ], +) + +ts_library( + name = "test_lib", + testonly = True, + srcs = glob(["*_spec.ts"]), + tags = [ + "ivy-only", + ], + deps = [ + "//packages:types", + "//packages/compiler", + "//packages/core", + "//packages/core/testing", + "//packages/private/testing", + ], +) + +jasmine_node_test( + name = "test", + data = [ + ":bundle", + ":bundle.js", + ":bundle.min.js", + ":bundle.min_debug.js", + ], + tags = [ + "ivy-only", + ], + deps = [":test_lib"], +) + +genrule( + name = "tslib", + srcs = [ + "@ngdeps//node_modules/tslib:tslib.js", + ], + outs = [ + "tslib.js", + ], + cmd = "cp $< $@", + tags = [ + "ivy-only", + ], +) + +ts_devserver( + name = "devserver", + entry_module = "angular/packages/core/test/bundling/todo_i18n/index", + serving_path = "/bundle.min.js", + static_files = [ + "index.html", + ":tslib", + "todo.css", + "base.css", + ], + tags = [ + "ivy-only", + ], + deps = [":todo_i18n"], +) + +http_server( + name = "prodserver", + data = [ + "base.css", + "index.html", + "todo.css", + ":bundle.min.js.br", + ":bundle.min_debug.js", + ], + tags = [ + "ivy-only", + ], +) diff --git a/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md b/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md new file mode 100644 index 0000000000..c2baf47676 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md @@ -0,0 +1,36 @@ +# Outstanding on the `Todo` app + +## `Todo` app +- [X] Clicking archive removes todo item. +- [X] Update `Todo` app to match http://todomvc.com/. +- [ ] Make it work with `[(ngModel)]`. +- [ ] Make it work with `(keyup.Enter)`. + +## Compiler +- [ ] Remove ` tslib_1.__decorate([core_1.Input(), tslib_1.__metadata("design:type", Object)], TodoComponent.prototype, "todo", void 0);` from generated output. +- [ ] Allow compilation of `@angular/common` through ivy. + +## Ivy Runtime +- [X] Work on `ViewContainerRef` needs to cause change detection so that `todo` app renders correctly on first render. +- [X] The todo input value box is not correctly rendering to checked for completed tasks. +- [ ] `ViewContainerRef` must separate creation mode from update mode otherwise {{todo.done}} fails for `NgFor` because `todo` is not set during creation mode. +- [ ] Injector should be optional + +## Testing +- [ ] Create a debug mode which would publish components into DOM for easier writing of tests. + + +## Bazel + +- [ ] Have action verb on the `ng_rollup_bundle` to display source maps. + +# NOTES + +## Killing hung `iblaze` server + +At times the `iblaze run packages/core/test/bundling/todo:devserver` keeps running and holding onto +ports even after `ctrl-c`. This command kills the outstanding processes. + +``` +kill -9 $(ps aux | grep ibazel\\\|devserver | cut -c 17-23) +``` \ No newline at end of file diff --git a/packages/core/test/bundling/todo_i18n/base.css b/packages/core/test/bundling/todo_i18n/base.css new file mode 100644 index 0000000000..9f6ac1bd74 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} \ No newline at end of file diff --git a/packages/core/test/bundling/todo_i18n/index.html b/packages/core/test/bundling/todo_i18n/index.html new file mode 100644 index 0000000000..f902b92519 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/index.html @@ -0,0 +1,57 @@ + + + + + + Angular Todo Example + + + + + + + + + + + + + + + diff --git a/packages/core/test/bundling/todo_i18n/index.ts b/packages/core/test/bundling/todo_i18n/index.ts new file mode 100644 index 0000000000..1fe523c042 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/index.ts @@ -0,0 +1,181 @@ +/** + * @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 '@angular/core/test/bundling/util/src/reflect_metadata'; +import {CommonModule} from '@angular/common'; +import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; +// TODO(ocombe): replace this with the real runtime i18n service +import {localize} from './translations'; + +class Todo { + editing: boolean; + + // TODO(issue/24571): remove '!'. + private _title !: string; + get title() { return this._title; } + set title(value: string) { this._title = value.trim(); } + + constructor(title: string, public completed: boolean = false) { + this.editing = false; + this.title = title; + } +} + +@Injectable({providedIn: 'root'}) +class TodoStore { + todos: Array = [ + new Todo(localize('Demonstrate Components')), + new Todo(localize('Demonstrate Structural Directives'), true), + // Using a placeholder + new Todo(localize('Demonstrate {$value}', {value: 'NgModules'})), + new Todo(localize('Demonstrate zoneless change detection')), + new Todo(localize('Demonstrate internationalization')), + ]; + + private getWithCompleted(completed: boolean) { + return this.todos.filter((todo: Todo) => todo.completed === completed); + } + + allCompleted() { return this.todos.length === this.getCompleted().length; } + + setAllTo(completed: boolean) { this.todos.forEach((t: Todo) => t.completed = completed); } + + removeCompleted() { this.todos = this.getWithCompleted(false); } + + getRemaining() { return this.getWithCompleted(false); } + + getCompleted() { return this.getWithCompleted(true); } + + toggleCompletion(todo: Todo) { todo.completed = !todo.completed; } + + remove(todo: Todo) { this.todos.splice(this.todos.indexOf(todo), 1); } + + add(title: string) { this.todos.push(new Todo(title)); } +} + +@Component({ + selector: 'todo-app', + // TODO(misko): make this work with `[(ngModel)]` + encapsulation: ViewEncapsulation.None, + template: ` +
+
+

todos

+ +
+
+ +
    +
  • +
    + + + +
    + +
  • +
+
+
+ + {{todoStore.getRemaining().length}} {todoStore.getRemaining().length, plural, =1 {item left} other {items left}} + + +
+
+ `, + // TODO(misko): switch over to OnPush + // changeDetection: ChangeDetectionStrategy.OnPush +}) +class ToDoAppComponent { + newTodoText = ''; + + constructor(public todoStore: TodoStore) {} + + cancelEditingTodo(todo: Todo) { + todo.editing = false; + markDirty(this); + } + + finishUpdatingTodo(todo: Todo, editedTitle: string) { + editedTitle = editedTitle.trim(); + + if (editedTitle.length === 0) { + this.remove(todo); + } + + todo.title = editedTitle; + this.cancelEditingTodo(todo); + } + + editTodo(todo: Todo) { + todo.editing = true; + markDirty(this); + } + + removeCompleted() { + this.todoStore.removeCompleted(); + markDirty(this); + } + + toggleCompletion(todo: Todo) { + this.todoStore.toggleCompletion(todo); + markDirty(this); + } + + remove(todo: Todo) { + this.todoStore.remove(todo); + markDirty(this); + } + + addTodo() { + if (this.newTodoText.trim().length) { + this.todoStore.add(this.newTodoText); + this.newTodoText = ''; + } + markDirty(this); + } + + toggleAllTodos(checked: boolean) { + this.todoStore.setAllTo(checked); + markDirty(this); + } + + updateEditedTodoValue(todo: Todo, value: string) { + todo.title = value; + markDirty(this); + } + + updateNewTodoValue(value: string) { + this.newTodoText = value; + markDirty(this); + } +} + +@NgModule({declarations: [ToDoAppComponent], imports: [CommonModule]}) +class ToDoAppModule { +} + +renderComponent(ToDoAppComponent); diff --git a/packages/core/test/bundling/todo_i18n/todo.css b/packages/core/test/bundling/todo_i18n/todo.css new file mode 100644 index 0000000000..19dadcf7e4 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/todo.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 80px; + line-height: 80px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml;utf8,'); +} + +.todo-list li label { + white-space: pre-line; + word-break: break-all; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts new file mode 100644 index 0000000000..81d97c21ce --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts @@ -0,0 +1,36 @@ +/** + * @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 '@angular/compiler'; +import {ɵwhenRendered as whenRendered} from '@angular/core'; +import {getComponent} from '@angular/core/src/render3'; +import {withBody} from '@angular/private/testing'; +import * as path from 'path'; + +const PACKAGE = 'angular/packages/core/test/bundling/todo_i18n'; +const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js']; + +describe('functional test for todo i18n', () => { + BUNDLES.forEach(bundle => { + describe(bundle, () => { + it('should render todo i18n', withBody('', async() => { + require(path.join(PACKAGE, bundle)); + const toDoAppComponent = getComponent(document.querySelector('todo-app') !); + expect(document.body.textContent).toContain('liste de tâches'); + expect(document.body.textContent).toContain('Démontrer les components'); + expect(document.body.textContent).toContain('Démontrer NgModules'); + expect(document.body.textContent).toContain('4 tâches restantes'); + expect(document.querySelector('.new-todo') !.getAttribute('placeholder')) + .toEqual(`Qu'y a-t-il à faire ?`); + document.querySelector('button') !.click(); + await whenRendered(toDoAppComponent); + expect(document.body.textContent).toContain('3 tâches restantes'); + })); + }); + }); +}); diff --git a/packages/core/test/bundling/todo_i18n/translations.ts b/packages/core/test/bundling/todo_i18n/translations.ts new file mode 100644 index 0000000000..05de50a8c9 --- /dev/null +++ b/packages/core/test/bundling/todo_i18n/translations.ts @@ -0,0 +1,42 @@ +/** + * @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 + */ + +declare var global: any; +declare var window: any; + +export const translations: {[key: string]: string} = { + 'What needs to be done?': `Qu'y a-t-il à faire ?`, + '{$startHeadingLevel1}todos{$closeHeadingLevel1}{$tagInput}': + '{$startHeadingLevel1}liste de tâches{$closeHeadingLevel1}{$tagInput}', + '{VAR_PLURAL, plural, =1 {item left} other {items left}}': + '{VAR_PLURAL, plural, =1 {tâche restante} other {tâches restantes}}', + '{$startTagStrong}{$interpolation}{$closeTagStrong}{$icu}': + '{$startTagStrong}{$interpolation}{$closeTagStrong} {$icu}', + ' Clear completed ': ' Effacer terminés ', + 'Demonstrate Components': 'Démontrer les components', + 'Demonstrate Structural Directives': 'Démontrer les directives structurelles', + 'Demonstrate {$value}': 'Démontrer {$value}', + 'Demonstrate zoneless change detection': 'Démontrer la détection des changements sans zonejs', + 'Demonstrate internationalization': `Démontrer l'internationalisation` +}; + +// Runtime i18n uses Closure goog.getMsg for now +// It will be replaced by the runtime service for external people +const glob = typeof global !== 'undefined' ? global : window; +glob.goog = glob.goog || {}; +glob.goog.getMsg = + glob.goog.getMsg || function(input: string, placeholders: {[key: string]: string} = {}) { + if (typeof translations[input] !== 'undefined') { // to account for empty string + input = translations[input]; + } + return Object.keys(placeholders).length ? + input.replace(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') : + input; + }; + +export const localize = goog.getMsg; diff --git a/packages/goog.d.ts b/packages/goog.d.ts index 69473ca3d3..f347fc0f85 100644 --- a/packages/goog.d.ts +++ b/packages/goog.d.ts @@ -16,6 +16,7 @@ declare namespace goog { * as it is sometimes true. */ export const DEBUG: boolean; + export const getMsg: (input: string, placeholders?: {[key: string]: string}) => string; } /** diff --git a/packages/private/testing/src/goog_get_msg.ts b/packages/private/testing/src/goog_get_msg.ts index a4b63dcaf8..902339e955 100644 --- a/packages/private/testing/src/goog_get_msg.ts +++ b/packages/private/testing/src/goog_get_msg.ts @@ -13,11 +13,14 @@ * running outside of Closure Compiler. This method will not be needed once runtime translation * service support is introduced. */ -export function polyfillGoogGetMsg(): void { +export function polyfillGoogGetMsg(translations: {[key: string]: string} = {}): void { const glob = (global as any); glob.goog = glob.goog || {}; glob.goog.getMsg = glob.goog.getMsg || function(input: string, placeholders: {[key: string]: string} = {}) { + if (typeof translations[input] !== 'undefined') { // to account for empty string + input = translations[input]; + } return Object.keys(placeholders).length ? input.replace(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') : input;