From 3997d9780615dc323eb357bf7077d0264a1c35e9 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Fri, 3 Nov 2017 23:54:54 +0100 Subject: [PATCH] revert: feat(elements): implement `@angular/elements` #19469 (#20152) This PR was merged without API docs and general rollout plan. We can't release this as is in 5.1 without a plan for documentation, cli integration, etc. --- .pullapprove.yml | 8 - CONTRIBUTING.md | 1 - .../transforms/angular-api-package/index.js | 1 - .../transforms/authors-package/api-package.js | 5 +- build.sh | 3 +- karma-js.conf.js | 9 - package.json | 2 - packages/elements/index.ts | 14 - packages/elements/package.json | 22 - packages/elements/public_api.ts | 19 - packages/elements/rollup.config.js | 28 - .../elements/src/extract-projectable-nodes.ts | 54 - .../src/ng-element-application-context.ts | 18 - .../elements/src/ng-element-constructor.ts | 141 -- packages/elements/src/ng-element.ts | 367 ----- packages/elements/src/ng-elements.ts | 155 --- .../src/register-as-custom-elements.ts | 37 - packages/elements/src/utils.ts | 112 -- packages/elements/src/version.ts | 14 - .../test/extract-projectable-nodes_spec.ts | 83 -- .../ng-element-application-context_spec.ts | 52 - .../test/ng-element-constructor_spec.ts | 349 ----- packages/elements/test/ng-element_spec.ts | 1232 ----------------- packages/elements/test/ng-elements_spec.ts | 552 -------- .../test/register-as-custom-elements_spec.ts | 127 -- packages/elements/test/utils_spec.ts | 259 ---- packages/elements/testing/index.ts | 115 -- packages/elements/tsconfig-build.json | 27 - test-main.js | 140 +- tools/cjs-jasmine/index.ts | 1 - tools/gulp-tasks/public-api.js | 2 +- tools/public_api_guard/elements/elements.d.ts | 30 - .../commit-message.json | 1 - .../validate-commit-message.spec.js | 10 +- yarn.lock | 8 - 35 files changed, 23 insertions(+), 3975 deletions(-) delete mode 100644 packages/elements/index.ts delete mode 100644 packages/elements/package.json delete mode 100644 packages/elements/public_api.ts delete mode 100644 packages/elements/rollup.config.js delete mode 100644 packages/elements/src/extract-projectable-nodes.ts delete mode 100644 packages/elements/src/ng-element-application-context.ts delete mode 100644 packages/elements/src/ng-element-constructor.ts delete mode 100644 packages/elements/src/ng-element.ts delete mode 100644 packages/elements/src/ng-elements.ts delete mode 100644 packages/elements/src/register-as-custom-elements.ts delete mode 100644 packages/elements/src/utils.ts delete mode 100644 packages/elements/src/version.ts delete mode 100644 packages/elements/test/extract-projectable-nodes_spec.ts delete mode 100644 packages/elements/test/ng-element-application-context_spec.ts delete mode 100644 packages/elements/test/ng-element-constructor_spec.ts delete mode 100644 packages/elements/test/ng-element_spec.ts delete mode 100644 packages/elements/test/ng-elements_spec.ts delete mode 100644 packages/elements/test/register-as-custom-elements_spec.ts delete mode 100644 packages/elements/test/utils_spec.ts delete mode 100644 packages/elements/testing/index.ts delete mode 100644 packages/elements/tsconfig-build.json delete mode 100644 tools/public_api_guard/elements/elements.d.ts diff --git a/.pullapprove.yml b/.pullapprove.yml index 54b5a0f874..e5ec0433d6 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -279,14 +279,6 @@ groups: - IgorMinar #fallback - mhevery #fallback - elements: - conditions: - files: - - "packages/elements/*" - users: - - mhevery #primary - - IgorMinar #fallback - benchpress: conditions: files: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4278b57160..2a7ca51af1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -211,7 +211,6 @@ The following is the list of supported scopes: * **compiler** * **compiler-cli** * **core** -* **elements** * **forms** * **http** * **language-service** diff --git a/aio/tools/transforms/angular-api-package/index.js b/aio/tools/transforms/angular-api-package/index.js index e11b87c34b..3fa94b3c87 100644 --- a/aio/tools/transforms/angular-api-package/index.js +++ b/aio/tools/transforms/angular-api-package/index.js @@ -48,7 +48,6 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage]) 'common/testing/index.ts', 'core/index.ts', 'core/testing/index.ts', - 'elements/index.ts', 'forms/index.ts', 'http/index.ts', 'http/testing/index.ts', diff --git a/aio/tools/transforms/authors-package/api-package.js b/aio/tools/transforms/authors-package/api-package.js index 5f07254b22..541d1ac45e 100644 --- a/aio/tools/transforms/authors-package/api-package.js +++ b/aio/tools/transforms/authors-package/api-package.js @@ -11,16 +11,15 @@ const { API_SOURCE_PATH } = require('../config'); const packageMap = { animations: ['animations/index.ts', 'animations/browser/index.ts', 'animations/browser/testing/index.ts'], - common: ['common/index.ts', 'common/testing/index.ts', 'common/http/index.ts', 'common/http/testing/index.ts'], + common: ['common/index.ts', 'common/testing/index.ts'], core: ['core/index.ts', 'core/testing/index.ts'], - elements: ['elements/index.ts'], forms: ['forms/index.ts'], http: ['http/index.ts', 'http/testing/index.ts'], 'platform-browser': ['platform-browser/index.ts', 'platform-browser/animations/index.ts', 'platform-browser/testing/index.ts'], 'platform-browser-dynamic': ['platform-browser-dynamic/index.ts', 'platform-browser-dynamic/testing/index.ts'], 'platform-server': ['platform-server/index.ts', 'platform-server/testing/index.ts'], 'platform-webworker': ['platform-webworker/index.ts'], - 'platform-webworker-dynamic': ['platform-webworker-dynamic/index.ts'], + 'platform-webworker-dynamic': 'platform-webworker-dynamic/index.ts', router: ['router/index.ts', 'router/testing/index.ts', 'router/upgrade/index.ts'], 'service-worker': ['service-worker/index.ts'], upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts'] diff --git a/build.sh b/build.sh index 2a73041cde..654c861b96 100755 --- a/build.sh +++ b/build.sh @@ -24,8 +24,7 @@ PACKAGES=(core compiler-cli language-service benchpress - service-worker - elements) + service-worker) TSC_PACKAGES=(compiler-cli language-service diff --git a/karma-js.conf.js b/karma-js.conf.js index 347b5ab6a4..1f9429f186 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -38,15 +38,6 @@ module.exports = function(config) { 'test-events.js', 'shims_for_IE.js', 'node_modules/systemjs/dist/system.src.js', - - // Serve polyfills necessary for testing the `elements` package. - { - pattern: 'node_modules/@webcomponents/custom-elements/**/*.js', - included: false, - watched: false - }, - {pattern: 'node_modules/mutation-observer/index.js', included: false, watched: false}, - {pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true}, 'node_modules/reflect-metadata/Reflect.js', 'tools/build/file2modulename.js', diff --git a/package.json b/package.json index b8e68d2962..7eb1238574 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "@types/selenium-webdriver": "3.0.7", "@types/source-map": "^0.5.1", "@types/systemjs": "0.19.32", - "@webcomponents/custom-elements": "^1.0.4", "angular": "1.5.0", "angular-animate": "1.5.0", "angular-mocks": "1.5.0", @@ -82,7 +81,6 @@ "karma-sourcemap-loader": "0.3.6", "madge": "0.5.0", "minimist": "1.2.0", - "mutation-observer": "^1.0.3", "node-uuid": "1.4.8", "protractor": "5.1.2", "rewire": "2.5.2", diff --git a/packages/elements/index.ts b/packages/elements/index.ts deleted file mode 100644 index e727e2e8a7..0000000000 --- a/packages/elements/index.ts +++ /dev/null @@ -1,14 +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 - */ - -// This file is not used to build this module. It is only used during editing -// by the TypeScript language service and during build for verification. `ngc` -// replaces this file with production index.ts when it rewrites private symbol -// names. - -export * from './public_api'; diff --git a/packages/elements/package.json b/packages/elements/package.json deleted file mode 100644 index 81ee117cd0..0000000000 --- a/packages/elements/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@angular/elements", - "version": "0.0.0-PLACEHOLDER", - "description": "Angular - library for using Angular in a web browser", - "main": "./bundles/elements.umd.js", - "module": "./esm5/elements.js", - "es2015": "./esm2015/elements.js", - "typings": "./elements.d.ts", - "author": "angular", - "license": "MIT", - "dependencies": { - "tslib": "^1.7.1" - }, - "peerDependencies": { - "@angular/core": "0.0.0-PLACEHOLDER", - "@angular/platform-browser": "0.0.0-PLACEHOLDER" - }, - "repository": { - "type": "git", - "url": "https://github.com/angular/angular.git" - } -} diff --git a/packages/elements/public_api.ts b/packages/elements/public_api.ts deleted file mode 100644 index f502eaf43a..0000000000 --- a/packages/elements/public_api.ts +++ /dev/null @@ -1,19 +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 - */ - -/** - * @module - * @description - * Entry point for all public APIs of the `elements` package. - */ -export {NgElement, NgElementWithProps} from './src/ng-element'; -export {NgElementConstructor} from './src/ng-element-constructor'; -export {registerAsCustomElements} from './src/register-as-custom-elements'; -export {VERSION} from './src/version'; - -// This file only reexports content of the `src` folder. Keep it that way. diff --git a/packages/elements/rollup.config.js b/packages/elements/rollup.config.js deleted file mode 100644 index 3c4b229ddf..0000000000 --- a/packages/elements/rollup.config.js +++ /dev/null @@ -1,28 +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 - */ - -const resolve = require('rollup-plugin-node-resolve'); -const sourcemaps = require('rollup-plugin-sourcemaps'); - -const globals = { - '@angular/core': 'ng.core', - '@angular/platform-browser': 'ng.platformBrowser', - 'rxjs/Subscription': 'Rx', -}; - -module.exports = { - entry: '../../dist/packages-dist/elements/esm5/elements.js', - dest: '../../dist/packages-dist/elements/bundles/elements.umd.js', - format: 'umd', - exports: 'named', - amd: {id: '@angular/elements'}, - moduleName: 'ng.elements', - plugins: [resolve(), sourcemaps()], - external: Object.keys(globals), - globals: globals -}; diff --git a/packages/elements/src/extract-projectable-nodes.ts b/packages/elements/src/extract-projectable-nodes.ts deleted file mode 100644 index b6df05bcba..0000000000 --- a/packages/elements/src/extract-projectable-nodes.ts +++ /dev/null @@ -1,54 +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 - */ - -// NOTE: This is a (slightly improved) version of what is used in ngUpgrade's -// `DowngradeComponentAdapter`. -// TODO(gkalpak): Investigate if it makes sense to share the code. - -import {isElement, matchesSelector} from './utils'; - -export function extractProjectableNodes(host: HTMLElement, ngContentSelectors: string[]): Node[][] { - const nodes = host.childNodes; - const projectableNodes: Node[][] = ngContentSelectors.map(() => []); - let wildcardIndex = -1; - - ngContentSelectors.some((selector, i) => { - if (selector === '*') { - wildcardIndex = i; - return true; - } - return false; - }); - - for (let i = 0, ii = nodes.length; i < ii; ++i) { - const node = nodes[i]; - const ngContentIndex = findMatchingIndex(node, ngContentSelectors, wildcardIndex); - - if (ngContentIndex !== -1) { - projectableNodes[ngContentIndex].push(node); - } - } - - return projectableNodes; -} - -function findMatchingIndex(node: Node, selectors: string[], defaultIndex: number): number { - let matchingIndex = defaultIndex; - - if (isElement(node)) { - selectors.some((selector, i) => { - if ((selector !== '*') && matchesSelector(node, selector)) { - matchingIndex = i; - return true; - } - return false; - }); - } - - return matchingIndex; -} diff --git a/packages/elements/src/ng-element-application-context.ts b/packages/elements/src/ng-element-application-context.ts deleted file mode 100644 index 79a8d587e2..0000000000 --- a/packages/elements/src/ng-element-application-context.ts +++ /dev/null @@ -1,18 +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 {ApplicationRef, Injector, NgZone} from '@angular/core'; - -export class NgElementApplicationContext { - applicationRef = this.injector.get(ApplicationRef); - ngZone = this.injector.get(NgZone); - - constructor(public injector: Injector) {} - - runInNgZone(cb: () => R): R { return this.ngZone.run(cb); } -} diff --git a/packages/elements/src/ng-element-constructor.ts b/packages/elements/src/ng-element-constructor.ts deleted file mode 100644 index 0ea67aae49..0000000000 --- a/packages/elements/src/ng-element-constructor.ts +++ /dev/null @@ -1,141 +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 {ComponentFactory, EventEmitter} from '@angular/core'; - -import {NgElementImpl, NgElementWithProps} from './ng-element'; -import {NgElementApplicationContext} from './ng-element-application-context'; -import {camelToKebabCase, throwError} from './utils'; - -/** - * TODO(gkalpak): Add docs. - * @experimental - */ -export interface NgElementConstructor { - readonly is: string; - readonly observedAttributes: string[]; - - upgrade(host: HTMLElement): NgElementWithProps; - - new (): NgElementWithProps; -} - -export interface NgElementConstructorInternal extends NgElementConstructor { - readonly onConnected: EventEmitter>; - readonly onDisconnected: EventEmitter>; - upgrade(host: HTMLElement, ignoreUpgraded?: boolean): NgElementWithProps; -} - -type WithProperties

= { - [property in keyof P]: P[property] -}; - -// For more info on `PotentialCustomElementName` rules see: -// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name -const PCEN_RE = createPcenRe(); -const PCEN_BLACKLIST = [ - 'annotation-xml', - 'color-profile', - 'font-face', - 'font-face-src', - 'font-face-uri', - 'font-face-format', - 'font-face-name', - 'missing-glyph', -]; - -export function createNgElementConstructor( - appContext: NgElementApplicationContext, - componentFactory: ComponentFactory): NgElementConstructorInternal { - const selector = componentFactory.selector; - - if (!isPotentialCustomElementName(selector)) { - throwError( - `Using '${selector}' as a custom element name is not allowed. ` + - 'See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name for more info.'); - } - - const inputs = componentFactory.inputs.map(({propName, templateName}) => ({ - propName, - attrName: camelToKebabCase(templateName), - })); - const outputs = - componentFactory.outputs.map(({propName, templateName}) => ({ - propName, - // TODO(gkalpak): Verify this is what we want and document. - eventName: templateName, - })); - - // Note: According to the spec, this needs to be an ES2015 class - // (i.e. not transpiled to an ES5 constructor function). - // TODO(gkalpak): Document that if you are using ES5 sources you need to include a polyfill (e.g. - // https://github.com/webcomponents/custom-elements/blob/32f043c3a/src/native-shim.js). - class NgElementConstructorImpl extends NgElementImpl { - static readonly is = selector; - static readonly observedAttributes = inputs.map(input => input.attrName); - static readonly onConnected = new EventEmitter>(); - static readonly onDisconnected = new EventEmitter>(); - - static upgrade(host: HTMLElement, ignoreUpgraded = false): NgElementWithProps { - const ngElement = new NgElementConstructorImpl(); - - ngElement.setHost(host); - ngElement.connectedCallback(ignoreUpgraded); - - return ngElement as typeof ngElement & WithProperties

; - } - - constructor() { - super(appContext, componentFactory, inputs, outputs); - - const ngElement = this as this & WithProperties

; - this.onConnected.subscribe(() => NgElementConstructorImpl.onConnected.emit(ngElement)); - this.onDisconnected.subscribe(() => NgElementConstructorImpl.onDisconnected.emit(ngElement)); - } - } - - inputs.forEach(({propName}) => { - Object.defineProperty(NgElementConstructorImpl.prototype, propName, { - get: function(this: NgElementImpl) { return this.getInputValue(propName); }, - set: function(this: NgElementImpl, newValue: any) { - this.setInputValue(propName, newValue); - }, - configurable: true, - enumerable: true, - }); - }); - - return NgElementConstructorImpl as typeof NgElementConstructorImpl & { - new (): NgElementConstructorImpl&WithProperties

; - }; -} - -function createPcenRe() { - // According to [the - // spec](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name), - // `pcenChar` is allowed to contain Unicode characters in the 10000-EFFFF range. But in order to - // match this characters with a RegExp, we need the implementation to support the `u` flag. - // On browsers that do not support it, valid PotentialCustomElementNames using characters in the - // 10000-EFFFF range will still cause an error (but these characters are not expected to be used - // in practice). - let pcenChar = '-.0-9_a-z\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u037D\\u037F-\\u1FFF' + - '\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF' + - '\\uF900-\\uFDCF\\uFDF0-\\uFFFD'; - let flags = ''; - - if (RegExp.prototype.hasOwnProperty('unicode')) { - pcenChar += '\\u{10000}-\\u{EFFFF}'; - flags += 'u'; - } - - return RegExp(`^[a-z][${pcenChar}]*-[${pcenChar}]*$`, flags); -} - -function isPotentialCustomElementName(name: string): boolean { - return PCEN_RE.test(name) && (PCEN_BLACKLIST.indexOf(name) === -1); -} diff --git a/packages/elements/src/ng-element.ts b/packages/elements/src/ng-element.ts deleted file mode 100644 index 314791779f..0000000000 --- a/packages/elements/src/ng-element.ts +++ /dev/null @@ -1,367 +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 {ApplicationRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges} from '@angular/core'; -import {Subscription} from 'rxjs/Subscription'; - -import {extractProjectableNodes} from './extract-projectable-nodes'; -import {NgElementApplicationContext} from './ng-element-application-context'; -import {createCustomEvent, getComponentName, isFunction, scheduler, strictEquals, throwError} from './utils'; - -/** - * TODO(gkalpak): Add docs. - * @experimental - */ -export type NgElementWithProps = NgElement& {[property in keyof P]: P[property]}; - -/** - * TODO(gkalpak): Add docs. - * @experimental - */ -export interface NgElement extends HTMLElement { - ngElement: NgElement|null; - componentRef: ComponentRef|null; - - attributeChangedCallback( - attrName: string, oldValue: string|null, newValue: string, namespace?: string): void; - connectedCallback(): void; - detach(): void; - detectChanges(): void; - disconnectedCallback(): void; - getHost(): HTMLElement; - markDirty(): void; -} - -/** - * Represents an `NgElement` input. - * Similar to a `ComponentFactory` input (`{propName: string, templateName: string}`), - * except that `attrName` is derived by kebab-casing `templateName`. - */ -export interface NgElementInput { - propName: string; - attrName: string; -} - -/** - * Represents an `NgElement` input. - * Similar to a `ComponentFactory` output (`{propName: string, templateName: string}`), - * except that `templateName` is renamed to `eventName`. - */ -export interface NgElementOutput { - propName: string; - eventName: string; -} - -/** - * An enum of possible lifecycle phases for `NgElement`s. - */ -const enum NgElementLifecyclePhase { - // The element has been instantiated, but not connected. - // (The associated component has not been created yet.) - unconnected = 'unconnected', - // The element has been instantiated and connected. - // (The associated component has been created.) - connected = 'connected', - // The element has been instantiated, connected and then disconnected. - // (The associated component has been created and then destroyed.) - disconnected = 'disconnected', -} - -interface NgElementConnected extends NgElementImpl { - ngElement: NgElementConnected; - componentRef: ComponentRef; -} - -export abstract class NgElementImpl extends HTMLElement implements NgElement { - private static DESTROY_DELAY = 10; - ngElement: NgElement|null = null; - componentRef: ComponentRef|null = null; - onConnected = new EventEmitter(); - onDisconnected = new EventEmitter(); - - private host = this as HTMLElement; - private readonly componentName = getComponentName(this.componentFactory.componentType); - private readonly initialInputValues = new Map(); - private readonly uninitializedInputs = new Set(); - private readonly outputSubscriptions = new Map(); - private inputChanges: SimpleChanges|null = null; - private implementsOnChanges = false; - private changeDetectionScheduled = false; - private lifecyclePhase: NgElementLifecyclePhase = NgElementLifecyclePhase.unconnected; - private cancelDestruction: (() => void)|null = null; - - constructor( - private appContext: NgElementApplicationContext, - private componentFactory: ComponentFactory, private readonly inputs: NgElementInput[], - private readonly outputs: NgElementOutput[]) { - super(); - } - - attributeChangedCallback( - attrName: string, oldValue: string|null, newValue: string, namespace?: string): void { - const input = this.inputs.find(input => input.attrName === attrName) !; - - if (input) { - this.setInputValue(input.propName, newValue); - } else { - throwError( - `Calling 'attributeChangedCallback()' with unknown attribute '${attrName}' ` + - `on component '${this.componentName}' is not allowed.`); - } - } - - connectedCallback(ignoreUpgraded = false): void { - this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'connectedCallback'); - - if (this.cancelDestruction !== null) { - this.cancelDestruction(); - this.cancelDestruction = null; - } - - if (this.lifecyclePhase === NgElementLifecyclePhase.connected) { - return; - } - - const host = this.host as NgElement; - - if (host.ngElement) { - if (ignoreUpgraded) { - return; - } - - const existingNgElement = (host as NgElementConnected).ngElement; - const existingComponentName = getComponentName(existingNgElement.componentRef.componentType); - - throwError( - `Upgrading '${this.host.nodeName}' element to component '${this.componentName}' is not allowed, ` + - `because the element is already upgraded to component '${existingComponentName}'.`); - } - - this.appContext.runInNgZone(() => { - this.lifecyclePhase = NgElementLifecyclePhase.connected; - const cThis = (this as any as NgElementConnected); - - const childInjector = Injector.create([], cThis.appContext.injector); - const projectableNodes = - extractProjectableNodes(cThis.host, cThis.componentFactory.ngContentSelectors); - cThis.componentRef = - cThis.componentFactory.create(childInjector, projectableNodes, cThis.host); - cThis.implementsOnChanges = - isFunction((cThis.componentRef.instance as any as OnChanges).ngOnChanges); - - cThis.initializeInputs(); - cThis.initializeOutputs(); - cThis.detectChanges(); - - cThis.appContext.applicationRef.attachView(cThis.componentRef.hostView); - - // Ensure `ngElement` is set on the host too (even for manually upgraded elements) - // in order to be able to detect that the element has been been upgraded. - cThis.ngElement = host.ngElement = cThis; - - cThis.onConnected.emit(); - }); - } - - detach(): void { this.disconnectedCallback(); } - - detectChanges(): void { - if (this.lifecyclePhase === NgElementLifecyclePhase.disconnected) { - return; - } - - this.assertNotInPhase(NgElementLifecyclePhase.unconnected, 'detectChanges'); - - this.appContext.runInNgZone(() => { - const cThis = this as any as NgElementConnected; - - cThis.changeDetectionScheduled = false; - - cThis.callNgOnChanges(); - cThis.componentRef.changeDetectorRef.detectChanges(); - }); - } - - disconnectedCallback(): void { - if (this.lifecyclePhase === NgElementLifecyclePhase.disconnected || - this.cancelDestruction !== null) { - return; - } - - this.assertNotInPhase(NgElementLifecyclePhase.unconnected, 'disconnectedCallback'); - - const doDestroy = () => this.appContext.runInNgZone(() => this.destroy()); - this.cancelDestruction = scheduler.schedule(doDestroy, NgElementImpl.DESTROY_DELAY); - } - - getHost(): HTMLElement { return this.host; } - - getInputValue(propName: string): any { - this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'getInputValue'); - - if (this.lifecyclePhase === NgElementLifecyclePhase.unconnected) { - return this.initialInputValues.get(propName); - } - - const cThis = this as any as NgElementConnected; - - return (cThis.componentRef.instance as any)[propName]; - } - - markDirty(): void { - if (!this.changeDetectionScheduled) { - this.changeDetectionScheduled = true; - scheduler.scheduleBeforeRender(() => this.detectChanges()); - } - } - - setHost(host: HTMLElement): void { - this.assertNotInPhase(NgElementLifecyclePhase.connected, 'setHost'); - this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'setHost'); - - this.host = host; - } - - setInputValue(propName: string, newValue: any): void { - this.assertNotInPhase(NgElementLifecyclePhase.disconnected, 'setInputValue'); - - if (this.lifecyclePhase === NgElementLifecyclePhase.unconnected) { - this.initialInputValues.set(propName, newValue); - return; - } - - const cThis = this as any as NgElementConnected; - - if (!strictEquals(newValue, cThis.getInputValue(propName))) { - cThis.recordInputChange(propName, newValue); - (cThis.componentRef.instance as any)[propName] = newValue; - cThis.markDirty(); - } - } - - private assertNotInPhase(phase: NgElementLifecyclePhase, caller: keyof this): void { - if (this.lifecyclePhase === phase) { - throwError( - `Calling '${caller}()' on ${phase} component '${this.componentName}' is not allowed.`); - } - } - - private callNgOnChanges(this: NgElementConnected): void { - if (this.implementsOnChanges && this.inputChanges !== null) { - const inputChanges = this.inputChanges; - this.inputChanges = null; - (this.componentRef.instance as any as OnChanges).ngOnChanges(inputChanges); - } - } - - private destroy() { - const cThis = this as any as NgElementConnected; - - cThis.componentRef.destroy(); - cThis.outputs.forEach(output => cThis.unsubscribeFromOutput(output)); - - this.ngElement = (this.host as NgElement).ngElement = null; - cThis.host.innerHTML = ''; - - cThis.lifecyclePhase = NgElementLifecyclePhase.disconnected; - cThis.onDisconnected.emit(); - } - - private dispatchCustomEvent(eventName: string, value: any): void { - const event = createCustomEvent(this.host.ownerDocument, eventName, value); - - this.dispatchEvent(event); - - if (this.host !== this) { - this.host.dispatchEvent(event); - } - } - - private initializeInputs(): void { - this.inputs.forEach(({propName, attrName}) => { - let initialValue; - - if (this.initialInputValues.has(propName)) { - // The property has already been set (prior to initialization). - // Update the component instance. - initialValue = this.initialInputValues.get(propName); - } else if (this.host.hasAttribute(attrName)) { - // A matching attribute exists. - // Update the component instance. - initialValue = this.host.getAttribute(attrName); - } else { - // The property does not have an initial value. - this.uninitializedInputs.add(propName); - } - - if (!this.uninitializedInputs.has(propName)) { - // The property does have an initial value. - // Forward it to the component instance. - this.setInputValue(propName, initialValue); - } - }); - - this.initialInputValues.clear(); - } - - private initializeOutputs(this: NgElementConnected): void { - this.outputs.forEach(output => this.subscribeToOutput(output)); - } - - private recordInputChange(propName: string, currentValue: any): void { - if (!this.implementsOnChanges) { - // The component does not implement `OnChanges`. Ignore the change. - return; - } - - if (this.inputChanges === null) { - this.inputChanges = {}; - } - - const pendingChange = this.inputChanges[propName]; - - if (pendingChange) { - pendingChange.currentValue = currentValue; - return; - } - - const isFirstChange = this.uninitializedInputs.has(propName); - const previousValue = isFirstChange ? undefined : this.getInputValue(propName); - this.inputChanges[propName] = new SimpleChange(previousValue, currentValue, isFirstChange); - - if (isFirstChange) { - this.uninitializedInputs.delete(propName); - } - } - - private subscribeToOutput(this: NgElementConnected, output: NgElementOutput): void { - const {propName, eventName} = output; - const emitter = (this.componentRef.instance as any)[output.propName] as EventEmitter; - - if (!emitter) { - throwError(`Missing emitter '${propName}' on component '${this.componentName}'.`); - } - - this.unsubscribeFromOutput(output); - - const subscription = - emitter.subscribe((value: any) => this.dispatchCustomEvent(eventName, value)); - this.outputSubscriptions.set(propName, subscription); - } - - private unsubscribeFromOutput({propName}: NgElementOutput): void { - if (!this.outputSubscriptions.has(propName)) { - return; - } - - const subscription = this.outputSubscriptions.get(propName) !; - - this.outputSubscriptions.delete(propName); - subscription.unsubscribe(); - } -} diff --git a/packages/elements/src/ng-elements.ts b/packages/elements/src/ng-elements.ts deleted file mode 100644 index ea82ae1120..0000000000 --- a/packages/elements/src/ng-elements.ts +++ /dev/null @@ -1,155 +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 {ComponentFactoryResolver, NgModuleRef, Type} from '@angular/core'; -import {DOCUMENT} from '@angular/platform-browser'; - -import {NgElement} from './ng-element'; -import {NgElementApplicationContext} from './ng-element-application-context'; -import {NgElementConstructor, NgElementConstructorInternal, createNgElementConstructor} from './ng-element-constructor'; -import {scheduler, throwError} from './utils'; - -/** - * TODO(gkalpak): Add docs. - * @experimental - */ -export class NgElements { - private doc = this.moduleRef.injector.get(DOCUMENT); - private definitions = new Map>(); - private upgradedElements = new Set>(); - private appContext = new NgElementApplicationContext(this.moduleRef.injector); - private changeDetectionScheduled = false; - - constructor(public readonly moduleRef: NgModuleRef, customElementComponents: Type[]) { - const resolver = moduleRef.componentFactoryResolver; - customElementComponents.forEach( - componentType => this.defineNgElement(this.appContext, resolver, componentType)); - } - - detachAll(root: Element = this.doc.documentElement): void { - const upgradedElements = Array.from(this.upgradedElements.values()); - const elementsToDetach: NgElement[] = []; - - this.traverseTree(root, (node: HTMLElement) => { - upgradedElements.some(ngElement => { - if (ngElement.getHost() === node) { - elementsToDetach.push(ngElement); - return true; - } - return false; - }); - }); - - // Detach in reverse traversal order. - this.appContext.runInNgZone( - () => elementsToDetach.reverse().forEach(ngElement => ngElement.detach())); - } - - detectChanges(): void { - this.changeDetectionScheduled = false; - this.appContext.runInNgZone( - () => this.upgradedElements.forEach(ngElement => ngElement.detectChanges())); - } - - forEach( - cb: - (def: NgElementConstructor, selector: string, - map: Map>) => void): void { - return this.definitions.forEach(cb); - } - - get(selector: string): NgElementConstructor|undefined { - return this.definitions.get(selector); - } - - markDirty(): void { - if (!this.changeDetectionScheduled) { - this.changeDetectionScheduled = true; - scheduler.scheduleBeforeRender(() => this.detectChanges()); - } - } - - register(customElements?: CustomElementRegistry): void { - if (!customElements && (typeof window !== 'undefined')) { - customElements = window.customElements; - } - - if (!customElements) { - throwError('Custom Elements are not supported in this environment.'); - } - - this.definitions.forEach(def => customElements !.define(def.is, def)); - } - - upgradeAll(root: Element = this.doc.documentElement): void { - const definitions = Array.from(this.definitions.values()); - - this.appContext.runInNgZone(() => { - this.traverseTree(root, (node: HTMLElement) => { - const nodeName = node.nodeName.toLowerCase(); - definitions.some(def => { - if (def.is === nodeName) { - // TODO(gkalpak): What happens if `node` contains more custom elements - // (as projectable content)? - def.upgrade(node, true); - return true; - } - return false; - }); - }); - }); - } - - private defineNgElement( - appContext: NgElementApplicationContext, resolver: ComponentFactoryResolver, - componentType: Type): void { - const componentFactory = resolver.resolveComponentFactory(componentType); - const def = createNgElementConstructor(appContext, componentFactory); - const selector = def.is; - - if (this.definitions.has(selector)) { - throwError( - `Defining an Angular custom element with selector '${selector}' is not allowed, ` + - 'because one is already defined.'); - } - - def.onConnected.subscribe((ngElement: NgElement) => this.upgradedElements.add(ngElement)); - def.onDisconnected.subscribe( - (ngElement: NgElement) => this.upgradedElements.delete(ngElement)); - - this.definitions.set(selector, def); - } - - // TODO(gkalpak): Add support for traversing through `shadowRoot` - // (as should happen according to the spec). - // TODO(gkalpak): Investigate security implications (e.g. as seen in - // https://github.com/angular/angular.js/pull/15699). - private traverseTree(root: Element, cb: (node: HTMLElement) => void): void { - let currentNode: Element|null = root; - - const getNextNonDescendant = (node: Element): Element | null => { - let currNode: Element|null = node; - let nextNode: Element|null = null; - - while (!nextNode && currNode && (currNode !== root)) { - nextNode = currNode.nextElementSibling; - currNode = currNode.parentElement; - } - - return nextNode; - }; - - while (currentNode) { - if (currentNode instanceof HTMLElement) { - cb(currentNode); - } - - currentNode = currentNode.firstElementChild || getNextNonDescendant(currentNode); - } - } -} diff --git a/packages/elements/src/register-as-custom-elements.ts b/packages/elements/src/register-as-custom-elements.ts deleted file mode 100644 index 0d10074183..0000000000 --- a/packages/elements/src/register-as-custom-elements.ts +++ /dev/null @@ -1,37 +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 {NgModuleFactory, NgModuleRef, PlatformRef, Type} from '@angular/core'; - -import {NgElements} from './ng-elements'; -import {isFunction} from './utils'; - -/** - * TODO(gkalpak): Add docs. - * @experimental - */ -export function registerAsCustomElements( - customElementComponents: Type[], platformRef: PlatformRef, - moduleFactory: NgModuleFactory): Promise>; -export function registerAsCustomElements( - customElementComponents: Type[], - bootstrapFn: () => Promise>): Promise>; -export function registerAsCustomElements( - customElementComponents: Type[], - platformRefOrBootstrapFn: PlatformRef | (() => Promise>), - moduleFactory?: NgModuleFactory): Promise> { - const bootstrapFn = isFunction(platformRefOrBootstrapFn) ? - platformRefOrBootstrapFn : - () => platformRefOrBootstrapFn.bootstrapModuleFactory(moduleFactory !); - - return bootstrapFn().then(moduleRef => { - const ngElements = new NgElements(moduleRef, customElementComponents); - ngElements.register(); - return moduleRef; - }); -} diff --git a/packages/elements/src/utils.ts b/packages/elements/src/utils.ts deleted file mode 100644 index c27ab3fb2e..0000000000 --- a/packages/elements/src/utils.ts +++ /dev/null @@ -1,112 +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 {Type} from '@angular/core'; - -const elProto = Element.prototype as any; -const matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector || - elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector; - -/** - * Provide methods for scheduling the execution of a callback. - */ -export const scheduler = { - /** - * Schedule a callback to be called after some delay. - */ - schedule(cb: () => void, delay: number): () => - void{const id = window.setTimeout(cb, delay); return () => window.clearTimeout(id);}, - - /** - * Schedule a callback to be called before the next render. - * (If `window.requestAnimationFrame()` is not available, use `scheduler.schedule()` instead.) - */ - scheduleBeforeRender(cb: () => void): () => void{ - // TODO(gkalpak): Implement a better way of accessing `requestAnimationFrame()` - // (e.g. accounting for vendor prefix, SSR-compatibility, etc). - if (typeof window.requestAnimationFrame === 'undefined') { - return scheduler.schedule(cb, 16); - } const id = window.requestAnimationFrame(cb); - return () => window.cancelAnimationFrame(id); - }, -}; - -/** - * Convert a camelCased string to kebab-cased. - */ -export function camelToKebabCase(input: string): string { - return input.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`); -} - -/** - * Create a `CustomEvent` (even on browsers where `CustomEvent` is not a constructor). - */ -export function createCustomEvent(doc: Document, name: string, detail: any): CustomEvent { - const bubbles = false; - const cancelable = false; - - // On IE9-11, `CustomEvent` is not a constructor. - if (typeof CustomEvent !== 'function') { - const event = doc.createEvent('CustomEvent'); - event.initCustomEvent(name, bubbles, cancelable, detail); - return event; - } - - return new CustomEvent(name, {bubbles, cancelable, detail}); -} - -/** - * Return the name of the component or the first line of its stringified version. - */ -export function getComponentName(component: Type): string { - return (component as any).overriddenName || component.name || - component.toString().split('\n', 1)[0]; -} - -/** - * Check whether the input is an `Element`. - */ -export function isElement(node: Node): node is Element { - return node.nodeType === Node.ELEMENT_NODE; -} - -/** - * Check whether the input is a function. - */ -export function isFunction(value: any): value is Function { - return typeof value === 'function'; -} - -/** - * Convert a kebab-cased string to camelCased. - */ -export function kebabToCamelCase(input: string): string { - return input.replace(/-([a-z\d])/g, (_, char) => char.toUpperCase()); -} - -/** - * Check whether an `Element` matches a CSS selector. - */ -export function matchesSelector(element: Element, selector: string): boolean { - return matches.call(element, selector); -} - -/** - * Test two values for strict equality, accounting for the fact that `NaN !== NaN`. - */ -export function strictEquals(value1: any, value2: any): boolean { - return value1 === value2 || (value1 !== value1 && value2 !== value2); -} - -/** - * Throw an error with the specified message. - * (It provides a centralized place where it is easy to apply some change/behavior to all errors.) - */ -export function throwError(message: string): void { - throw Error(message); -} diff --git a/packages/elements/src/version.ts b/packages/elements/src/version.ts deleted file mode 100644 index 8915bc7c70..0000000000 --- a/packages/elements/src/version.ts +++ /dev/null @@ -1,14 +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 {Version} from '@angular/core'; - -/** - * @experimental - */ -export const VERSION = new Version('0.0.0-PLACEHOLDER'); diff --git a/packages/elements/test/extract-projectable-nodes_spec.ts b/packages/elements/test/extract-projectable-nodes_spec.ts deleted file mode 100644 index 9d79760916..0000000000 --- a/packages/elements/test/extract-projectable-nodes_spec.ts +++ /dev/null @@ -1,83 +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 {extractProjectableNodes} from '../src/extract-projectable-nodes'; - -export function main() { - describe('extractProjectableNodes()', () => { - let elem: HTMLElement; - let childNodes: NodeList; - - const expectProjectableNodes = (matches: {[selector: string]: number[]}) => { - const selectors = Object.keys(matches); - const expected = selectors.map(selector => { - const matchingIndices = matches[selector]; - return matchingIndices.map(idx => childNodes[idx]); - }); - - expect(extractProjectableNodes(elem, selectors)).toEqual(expected); - }; - const test = (matches: {[selector: string]: number[]}) => () => expectProjectableNodes(matches); - - beforeEach(() => { - elem = document.createElement('div'); - elem.innerHTML = '

' + - '' + - '
' + - '' + - '' + - 'Text' + - '' + - 'More text'; - childNodes = Array.prototype.slice.call(elem.childNodes); - }); - - it('should match each node to the corresponding selector', test({ - '[first]': [0], - '#bar': [1], - '#quux': [4], - })); - - it('should ignore non-matching nodes', test({ - '.zoo': [], - })); - - it('should only match top-level child nodes', test({ - 'span': [1], - '.bar': [], - })); - - it('should support complex selectors', test({ - '.foo:not(div)': [4], - 'div + #bar': [1], - })); - - it('should match each node with the first matching selector', test({ - 'div': [0], - '.foo': [4], - 'blink': [], - })); - - describe('(with wildcard selector)', () => { - it('should match non-element nodes to `*` (but still ignore comments)', test({ - 'div,span,blink': [0, 1, 4], - '*': [2, 3, 5], - })); - - it('should match otherwise unmatched nodes to `*`', test({ - 'div,blink': [0, 4], - '*': [1, 2, 3, 5], - })); - - it('should give higher priority to `*` (eve if it appears first)', test({ - '*': [2, 3, 5], - 'div,span,blink': [0, 1, 4], - })); - }); - }); -} diff --git a/packages/elements/test/ng-element-application-context_spec.ts b/packages/elements/test/ng-element-application-context_spec.ts deleted file mode 100644 index 5c40891fd5..0000000000 --- a/packages/elements/test/ng-element-application-context_spec.ts +++ /dev/null @@ -1,52 +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 {ApplicationRef, Injector, NgZone} from '@angular/core'; -import {NgElementApplicationContext} from '../src/ng-element-application-context'; - -export function main() { - describe('NgElementApplicationContext', () => { - let mockInjector: Injector; - let mockZone: NgZone; - let ctx: NgElementApplicationContext; - - beforeEach(() => { - mockZone = new NgZone({}); - mockInjector = Injector.create([ - {provide: ApplicationRef, useValue: 'mockApplicationRef'}, - {provide: NgZone, useValue: mockZone}, - ]); - - ctx = new NgElementApplicationContext(mockInjector); - }); - - it('should expose the `ApplicationRef`', - () => { expect(ctx.applicationRef as any).toBe('mockApplicationRef'); }); - - it('should expose the `Injector`', () => { expect(ctx.injector).toBe(mockInjector); }); - - it('should expose the `NgZone`', () => { expect(ctx.ngZone).toBe(mockZone); }); - - describe('runInNgZone()', () => { - it('should always run the callback inside the Angular zone', () => { - (spyOn(NgZone, 'isInAngularZone').and as any).returnValues(false, true); - spyOn(mockZone, 'run').and.callThrough(); - const callbackSpy = (jasmine.createSpy('callback').and as any).returnValues('foo', 'bar'); - - const retValues = [ - ctx.runInNgZone(callbackSpy), - ctx.runInNgZone(callbackSpy), - ]; - - expect(mockZone.run).toHaveBeenCalledTimes(2); - expect(callbackSpy).toHaveBeenCalledTimes(2); - expect(retValues).toEqual(['foo', 'bar']); - }); - }); - }); -} diff --git a/packages/elements/test/ng-element-constructor_spec.ts b/packages/elements/test/ng-element-constructor_spec.ts deleted file mode 100644 index c9dee06793..0000000000 --- a/packages/elements/test/ng-element-constructor_spec.ts +++ /dev/null @@ -1,349 +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 {ApplicationRef, Component, ComponentFactory, EventEmitter, Inject, Input, NgModule, NgModuleRef, NgZone, Output, destroyPlatform} from '@angular/core'; -import {BrowserModule} from '@angular/platform-browser'; -import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {Subscription} from 'rxjs/Subscription'; - -import {NgElementImpl, NgElementWithProps} from '../src/ng-element'; -import {NgElementApplicationContext} from '../src/ng-element-application-context'; -import {NgElementConstructorInternal, createNgElementConstructor} from '../src/ng-element-constructor'; -import {installMockScheduler, patchEnv, restoreEnv, supportsCustomElements} from '../testing/index'; - -type WithFooBar = { - fooFoo: string, - barBar: string -}; - -export function main() { - if (!supportsCustomElements()) { - return; - } - - describe('NgElementConstructor', () => { - let moduleRef: NgModuleRef; - let c: NgElementConstructorInternal; - - beforeAll(() => patchEnv()); - beforeAll(done => { - installMockScheduler(true); - - destroyPlatform(); - platformBrowserDynamic() - .bootstrapModule(TestModule) - .then(ref => { - moduleRef = ref; - - const appContext = new NgElementApplicationContext(ref.injector); - const factory = ref.componentFactoryResolver.resolveComponentFactory(TestComponent); - - c = createNgElementConstructor(appContext, factory); - - // The `@webcomponents/custom-elements/src/native-shim.js` polyfill, that we use to - // enable ES2015 classes transpiled to ES5 constructor functions to be used as Custom - // Elements in tests, only works if the elements have been registered with - // `customElements.define()`. - customElements.define(c.is, c); - }) - .then(done, done.fail); - }); - - afterAll(() => destroyPlatform()); - afterAll(() => restoreEnv()); - - describe('is', () => { - it('should be derived from the component\'s selector', - () => { expect(c.is).toBe('test-component-for-ngec'); }); - - it('should be a valid custom element name', () => { - const buildTestFn = (selector: string) => { - const mockAppContext = {} as NgElementApplicationContext; - const mockFactory = { selector } as ComponentFactory; - return () => createNgElementConstructor(mockAppContext, mockFactory); - }; - const buildError = (selector: string) => - `Using '${selector}' as a custom element name is not allowed. ` + - 'See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name for more info.'; - - const validNames = [ - 'foo-bar', - 'baz-', - 'q-u-x', - 'this_is-fine.too', - 'this_is-fine.3', - 'this.is-φίνε.4', - 'tΉΪς.is-Φine.5', - ]; - const invalidNames = [ - 'foo', - 'BAR', - 'baz-Qux', - 'φine-not', - 'not:fine-at:all', - '.no-no', - '[nay-nay]', - 'close-but,not-quite', - ':not(my-element)', - // Blacklisted: - 'color-profile', - 'font-face-format', - 'missing-glyph', - ]; - - validNames.forEach(name => expect(buildTestFn(name)).not.toThrowError(buildError(name))); - invalidNames.forEach(name => expect(buildTestFn(name)).toThrowError(buildError(name))); - }); - }); - - describe('observedAttributes', () => { - it('should be derived from the component\'s inputs', () => { - expect(c.observedAttributes).toEqual(['foo-foo', 'barbar']); - }); - }); - - describe('constructor()', () => { - let e: NgElementWithProps; - - beforeEach(() => { - e = new c(); - e.connectedCallback(); - }); - - it('should create an `NgElement`', () => { - // When using the `Object.setPrototypeOf()` shim, we can't check for the `NgElementImpl` - // prototype. Check for `HTMLElement` instead. - const ParentClass = (Object as any).setPrototypeOf.$$shimmed ? HTMLElement : NgElementImpl; - - expect(e).toEqual(jasmine.any(ParentClass)); - expect(e.getHost()).toBe(e); - expect(e.ngElement).toBe(e); - }); - - it('should pass `ApplicationRef` to the element', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - const component = e.componentRef !.instance; - - component.fooFoo = 'newFoo'; - component.barBar = 'newBar'; - expect(e.innerHTML).toBe('TestComponent|foo(foo)|bar()'); - - appRef.tick(); - expect(e.innerHTML).toBe('TestComponent|foo(newFoo)|bar(newBar)'); - }); - - it('should pass `NgModuleRef` injector to the element', () => { - const component = e.componentRef !.instance; - expect(component.testValue).toBe('TEST'); - }); - - it('should pass appropriate inputs to the element', () => { - const component = e.componentRef !.instance; - - expect(component.fooFoo).toBe('foo'); - expect(component.barBar).toBeUndefined(); - - e.attributeChangedCallback('foo-foo', null, 'newFoo'); - expect(component.fooFoo).toBe('newFoo'); - - e.attributeChangedCallback('barbar', null, 'newBar'); - expect(component.barBar).toBe('newBar'); - }); - - it('should pass appropriate outputs to the element', () => { - const bazListener = jasmine.createSpy('bazListener'); - const quxListener = jasmine.createSpy('quxListener'); - const component = e.componentRef !.instance; - - e.addEventListener('bazBaz', bazListener); - e.addEventListener('quxqux', quxListener); - component.bazBaz.emit(false); - component.quxQux.emit({qux: true}); - - expect(bazListener).toHaveBeenCalledWith(jasmine.objectContaining({ - type: 'bazBaz', - detail: false, - })); - expect(quxListener).toHaveBeenCalledWith(jasmine.objectContaining({ - type: 'quxqux', - detail: {qux: true}, - })); - }); - - it('should set up property getters/setters for the inputs', () => { - const getInputValueSpy = - spyOn(e as any as NgElementImpl, 'getInputValue').and.callThrough(); - const setInputValueSpy = - spyOn(e as any as NgElementImpl, 'setInputValue').and.callThrough(); - - (e as any).randomProp = 'ignored'; - expect(setInputValueSpy).not.toHaveBeenCalled(); - expect((e as any).randomProp).toBe('ignored'); - expect(getInputValueSpy).not.toHaveBeenCalled(); - - e.fooFoo = 'newFoo'; - expect(setInputValueSpy).toHaveBeenCalledWith('fooFoo', 'newFoo'); - expect(e.fooFoo).toBe('newFoo'); - expect(getInputValueSpy).toHaveBeenCalledWith('fooFoo'); - - e.barBar = 'newBar'; - expect(setInputValueSpy).toHaveBeenCalledWith('barBar', 'newBar'); - expect(e.barBar).toBe('newBar'); - expect(getInputValueSpy).toHaveBeenCalledWith('barBar'); - }); - }); - - describe('upgrade()', () => { - let host: HTMLElement; - let e: NgElementWithProps; - - beforeEach(() => { - host = document.createElement('div'); - e = c.upgrade(host); - }); - - it('should create an `NgElement`', () => { - // When using the `Object.setPrototypeOf()` shim, we can't check for the `NgElementImpl` - // prototype. Check for `HTMLElement` instead. - const ParentClass = (Object as any).setPrototypeOf.$$shimmed ? HTMLElement : NgElementImpl; - - expect(e).toEqual(jasmine.any(ParentClass)); - }); - - it('should immediatelly instantiate the underlying component', () => { - expect(e.ngElement).toBe(e); - expect(e.getHost().innerHTML).toBe('TestComponent|foo(foo)|bar()'); - }); - - it('should use the specified host', () => { - expect(e.getHost()).toBe(host); - expect((host as typeof e).ngElement).toBe(e); - }); - - it('should throw if the host is already upgraded (ignoreUpgraded: false)', () => { - const errorMessage = - 'Upgrading \'DIV\' element to component \'TestComponent\' is not allowed, ' + - 'because the element is already upgraded to component \'TestComponent\'.'; - - expect(() => c.upgrade(host)).toThrowError(errorMessage); - expect(() => c.upgrade(host, false)).toThrowError(errorMessage); - }); - - it('should do nothing if the host is already upgraded (ignoreUpgraded: true)', () => { - const compRef = e.componentRef !; - - expect(() => c.upgrade(host, true)).not.toThrow(); - expect((host as typeof e).ngElement).toBe(e); - expect((host as typeof e).ngElement !.componentRef).toBe(compRef); - }); - }); - - describe('onConnected', () => { - let onConnectedSpy: jasmine.Spy; - let subscription: Subscription; - - beforeEach(() => { - onConnectedSpy = jasmine.createSpy('onConnected'); - subscription = c.onConnected.subscribe(onConnectedSpy); - }); - - afterEach(() => subscription.unsubscribe()); - - it('should emit every time an `NgElement` is connected', () => { - const e1 = new c(); - expect(onConnectedSpy).not.toHaveBeenCalled(); - - e1.connectedCallback(); - expect(onConnectedSpy).toHaveBeenCalledTimes(1); - expect(onConnectedSpy).toHaveBeenCalledWith(e1); - - onConnectedSpy.calls.reset(); - const e2 = c.upgrade(document.createElement('div')); - expect(onConnectedSpy).toHaveBeenCalledTimes(1); - expect(onConnectedSpy).toHaveBeenCalledWith(e2); - - onConnectedSpy.calls.reset(); - (e1 as any as NgElementImpl).onConnected.emit('ignored' as any); - expect(onConnectedSpy).toHaveBeenCalledTimes(1); - expect(onConnectedSpy).toHaveBeenCalledWith(e1); - - onConnectedSpy.calls.reset(); - (e2 as any as NgElementImpl).onConnected.emit('ignored' as any); - expect(onConnectedSpy).toHaveBeenCalledTimes(1); - expect(onConnectedSpy).toHaveBeenCalledWith(e2); - }); - }); - - describe('onDisconnected', () => { - let onDisconnectedSpy: jasmine.Spy; - let subscription: Subscription; - - beforeEach(() => { - onDisconnectedSpy = jasmine.createSpy('onDisconnected'); - subscription = c.onDisconnected.subscribe(onDisconnectedSpy); - }); - - afterEach(() => subscription.unsubscribe()); - - it('should emit every time an `NgElement` is disconnected', () => { - const e1 = new c(); - e1.connectedCallback(); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - - e1.disconnectedCallback(); - expect(onDisconnectedSpy).toHaveBeenCalledTimes(1); - expect(onDisconnectedSpy).toHaveBeenCalledWith(e1); - - onDisconnectedSpy.calls.reset(); - const e2 = c.upgrade(document.createElement('div')); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - - e2.disconnectedCallback(); - expect(onDisconnectedSpy).toHaveBeenCalledTimes(1); - expect(onDisconnectedSpy).toHaveBeenCalledWith(e2); - - onDisconnectedSpy.calls.reset(); - (e1 as any as NgElementImpl).onDisconnected.emit('ignored' as any); - expect(onDisconnectedSpy).toHaveBeenCalledTimes(1); - expect(onDisconnectedSpy).toHaveBeenCalledWith(e1); - - onDisconnectedSpy.calls.reset(); - (e2 as any as NgElementImpl).onDisconnected.emit('ignored' as any); - expect(onDisconnectedSpy).toHaveBeenCalledTimes(1); - expect(onDisconnectedSpy).toHaveBeenCalledWith(e2); - }); - }); - - // Helpers - @Component({ - selector: 'test-component-for-ngec', - template: 'TestComponent|foo({{ fooFoo }})|bar({{ barBar }})', - }) - class TestComponent { - @Input() fooFoo: string = 'foo'; - @Input('barbar') barBar: string; - - @Output() bazBaz = new EventEmitter(); - @Output('quxqux') quxQux = new EventEmitter(); - - constructor(@Inject('TEST_VALUE') public testValue: string) {} - } - - @NgModule({ - imports: [BrowserModule], - providers: [ - {provide: 'TEST_VALUE', useValue: 'TEST'}, - ], - declarations: [TestComponent], - entryComponents: [TestComponent], - }) - class TestModule { - ngDoBootstrap() {} - } - }); -} diff --git a/packages/elements/test/ng-element_spec.ts b/packages/elements/test/ng-element_spec.ts deleted file mode 100644 index 040b491061..0000000000 --- a/packages/elements/test/ng-element_spec.ts +++ /dev/null @@ -1,1232 +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 {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ApplicationRef, Component, ComponentFactory, DoCheck, EventEmitter, Inject, Injector, Input, NgModule, NgModuleRef, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChange, SimpleChanges, destroyPlatform} from '@angular/core'; -import {BrowserModule} from '@angular/platform-browser'; -import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {NgElementImpl, NgElementInput, NgElementOutput} from '../src/ng-element'; -import {NgElementApplicationContext} from '../src/ng-element-application-context'; -import {scheduler} from '../src/utils'; -import {AsyncMockScheduler, installMockScheduler, patchEnv, restoreEnv, supportsCustomElements} from '../testing/index'; - -type WithFooBar = { - foo: string, - bar: string -}; - -export function main() { - if (!supportsCustomElements()) { - return; - } - - describe('NgElement', () => { - const DESTROY_DELAY = 10; - const disconnectSync = (elem: NgElementImpl) => { - elem.disconnectedCallback(); - mockScheduler.tick(DESTROY_DELAY); - }; - - let mockScheduler: AsyncMockScheduler; - let moduleRef: NgModuleRef; - let nodeName: string; - let e: NgElementImpl&WithFooBar; - let host: HTMLElement; - - beforeAll(() => patchEnv()); - afterAll(() => restoreEnv()); - - [true, false].forEach(instantiateDirectly => { - [true, false].forEach(useItselfAsHost => { - const methodStr = instantiateDirectly ? 'directly' : 'with `document.createElement()`'; - const hostStr = useItselfAsHost ? 'itself' : 'another element'; - const description = `(instantiated ${methodStr} with ${hostStr} as host)`; - - describe(description, () => { - beforeEach(done => { - mockScheduler = installMockScheduler(); - - destroyPlatform(); - platformBrowserDynamic() - .bootstrapModule(TestModule) - .then(ref => { - moduleRef = ref; - nodeName = useItselfAsHost ? 'TEST-COMPONENT-FOR-NGE' : 'TEST-HOST'; - e = instantiateDirectly ? new TestNgElement() : - document.createElement(TestNgElement.is) as any; - - if (!useItselfAsHost) { - e.setHost(document.createElement('test-host')); - } - - host = e.getHost(); - }) - .then(done, done.fail); - }); - - afterEach(() => destroyPlatform()); - - it('should be an HTMLElement', () => { expect(e).toEqual(jasmine.any(HTMLElement)); }); - - it(`should have ${useItselfAsHost ? 'itself' : 'another element'} as host`, - () => { expect(host.nodeName).toBe(nodeName); }); - - describe('attributeChangedCallback()', () => { - let markDirtySpy: jasmine.Spy; - - beforeEach(() => markDirtySpy = spyOn(e, 'markDirty')); - - it('should update the corresponding property when unconnected', () => { - expect(e.foo).toBeUndefined(); - expect(e.bar).toBeUndefined(); - - e.attributeChangedCallback('foo', null, 'newFoo'); - e.attributeChangedCallback('b-a-r', null, 'newBar'); - - expect(e.foo).toBe('newFoo'); - expect(e.bar).toBe('newBar'); - - expect(markDirtySpy).not.toHaveBeenCalled(); - }); - - it('should update the corresponding property (when connected)', () => { - e.connectedCallback(); - - expect(e.foo).toBe('foo'); - expect(e.bar).toBeUndefined(); - - e.attributeChangedCallback('foo', null, 'newFoo'); - e.attributeChangedCallback('b-a-r', null, 'newBar'); - - expect(e.foo).toBe('newFoo'); - expect(e.bar).toBe('newBar'); - - expect(markDirtySpy).toHaveBeenCalledTimes(2); - }); - - it('should update the component instance (when connected)', () => { - e.connectedCallback(); - const component = e.componentRef !.instance; - - expect(component.foo).toBe('foo'); - expect(component.bar).toBeUndefined(); - - e.attributeChangedCallback('foo', null, 'newFoo'); - e.attributeChangedCallback('b-a-r', null, 'newBar'); - - expect(component.foo).toBe('newFoo'); - expect(component.bar).toBe('newBar'); - - expect(markDirtySpy).toHaveBeenCalledTimes(2); - }); - - it('should mark as dirty (when connected)', () => { - e.connectedCallback(); - e.attributeChangedCallback('foo', null, 'newFoo'); - e.attributeChangedCallback('b-a-r', null, 'newBar'); - - expect(markDirtySpy).toHaveBeenCalledTimes(2); - }); - - it('should not mark as dirty if the new value equals the old one', () => { - e.connectedCallback(); - - e.attributeChangedCallback('foo', null, 'newFoo'); - e.attributeChangedCallback('b-a-r', null, 'newBar'); - markDirtySpy.calls.reset(); - - e.attributeChangedCallback('foo', null, 'newFoo'); - e.attributeChangedCallback('b-a-r', null, 'newBar'); - expect(markDirtySpy).not.toHaveBeenCalled(); - }); - - it('should throw when disconnected', () => { - const errorMessage = - 'Calling \'setInputValue()\' on disconnected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - disconnectSync(e); - - const fn = () => e.attributeChangedCallback('foo', null, 'newFoo'); - - expect(fn).toThrowError(errorMessage); - expect(markDirtySpy).not.toHaveBeenCalled(); - }); - - it('should throw when called with unknown attribute', () => { - const fn = () => e.attributeChangedCallback('unknown', null, 'newUnknown'); - const errorMessage = - 'Calling \'attributeChangedCallback()\' with unknown attribute \'unknown\' ' + - 'on component \'TestComponent\' is not allowed.'; - - expect(fn).toThrowError(errorMessage); - expect((e as any).unknown).toBeUndefined(); - expect(markDirtySpy).not.toHaveBeenCalled(); - - e.connectedCallback(); - - expect(fn).toThrowError(errorMessage); - expect((e as any).unknown).toBeUndefined(); - expect(markDirtySpy).not.toHaveBeenCalled(); - - e.disconnectedCallback(); - - expect(fn).toThrowError(errorMessage); - expect((e as any).unknown).toBeUndefined(); - expect(markDirtySpy).not.toHaveBeenCalled(); - }); - }); - - describe('connectedCallback()', () => { - let detectChangesSpy: jasmine.Spy; - - beforeEach(() => detectChangesSpy = spyOn(e, 'detectChanges').and.callThrough()); - - it('should create the component', () => { - expect(e.componentRef).toBeNull(); - - e.connectedCallback(); - - const componentRef = e.componentRef !; - expect(componentRef).not.toBeNull(); - expect(componentRef.instance).toEqual(jasmine.any(TestComponent)); - expect(host.textContent).toContain('TestComponent'); - }); - - it('should instantiate the component inside the Angular zone', () => { - e.connectedCallback(); - - expect(NgZone.isInAngularZone()).toBe(false); - expect(e.componentRef !.instance.createdInNgZone).toBe(true); - }); - - it('should use the provided injector as parent', () => { - e.connectedCallback(); - - expect(e.componentRef !.instance.testValue).toBe('TEST'); - expect(e.componentRef !.injector).not.toBe(moduleRef.injector); - }); - - it('should project any content', () => { - host.innerHTML = 'rest-1' + - 'baz-1' + - 'rest-2' + - '
baz-2
'; - - e.connectedCallback(); - - expect(host.textContent) - .toBe('TestComponent|foo(foo)|bar()|baz(baz-1baz-2)|rest(rest-1rest-2)'); - }); - - it('should initialize component inputs with already set property values', () => { - e.foo = 'newFoo'; - e.bar = 'newBar'; - e.connectedCallback(); - - expect(e.componentRef !.instance.foo).toBe('newFoo'); - expect(e.componentRef !.instance.bar).toBe('newBar'); - }); - - it('should use the most recent property value', () => { - e.foo = 'newFoo'; - e.foo = 'newerFoo'; - e.foo = 'newestFoo'; - e.connectedCallback(); - - expect(e.componentRef !.instance.foo).toBe('newestFoo'); - }); - - it('should initialize component inputs from attributes (if properties are not set)', - () => { - host.setAttribute('foo', 'newFoo'); - host.setAttribute('b-a-r', 'newBar'); - host.setAttribute('bar', 'ignored'); - host.setAttribute('bA-r', 'ignored'); - e.connectedCallback(); - - expect(e.componentRef !.instance.foo).toBe('newFoo'); - expect(e.componentRef !.instance.bar).toBe('newBar'); - }); - - it('should prioritize properties over attributes (if both have been set)', () => { - host.setAttribute('foo', 'newFoo'); - host.setAttribute('b-a-r', 'newBar'); - e.bar = 'newerBar'; - e.connectedCallback(); - - expect(e.componentRef !.instance.foo).toBe('newFoo'); - expect(e.componentRef !.instance.bar).toBe('newerBar'); - }); - - it('should not ignore undefined as an input value', () => { - host.setAttribute('foo', 'newFoo'); - e.foo = 'newerFoo'; - e.foo = undefined as any; - e.connectedCallback(); - - expect(e.componentRef !.instance.foo).toBeUndefined(); - }); - - it('should convert component output emissions to custom events', () => { - const listeners = { - bazOnNgElement: jasmine.createSpy('bazOnNgElement'), - BAZOnNgElement: jasmine.createSpy('BAZOnNgElement'), - quxOnNgElement: jasmine.createSpy('quxOnNgElement'), - 'q-u-xOnNgElement': jasmine.createSpy('q-u-xOnNgElement'), - bazOnHost: jasmine.createSpy('bazOnHost'), - BAZOnHost: jasmine.createSpy('BAZOnHost'), - quxOnHost: jasmine.createSpy('quxOnHost'), - 'q-u-xOnHost': jasmine.createSpy('q-u-xOnHost'), - }; - - // Only events `baz` and `q-u-x` exist (regardless of pre-/post-connected phase). - e.addEventListener('baz', listeners.bazOnNgElement); - e.addEventListener('BAZ', listeners.BAZOnNgElement); - host.addEventListener('qux', listeners.quxOnHost); - host.addEventListener('q-u-x', listeners['q-u-xOnHost']); - e.connectedCallback(); - host.addEventListener('baz', listeners.bazOnHost); - host.addEventListener('BAZ', listeners.BAZOnHost); - e.addEventListener('qux', listeners.quxOnNgElement); - e.addEventListener('q-u-x', listeners['q-u-xOnNgElement']); - - Object.keys(listeners).forEach( - (k: keyof typeof listeners) => expect(listeners[k]).not.toHaveBeenCalled()); - - e.componentRef !.instance.baz.emit(false); - e.componentRef !.instance.qux.emit({qux: true}); - - ['BAZOnNgElement', 'BAZOnHost', 'quxOnNgElement', 'quxOnHost'].forEach( - (k: keyof typeof listeners) => expect(listeners[k]).not.toHaveBeenCalled()); - - expect(listeners.bazOnNgElement).toHaveBeenCalledTimes(1); - expect(listeners.bazOnNgElement).toHaveBeenCalledWith(jasmine.objectContaining({ - type: 'baz', - detail: false, - })); - expect(listeners.bazOnHost).toHaveBeenCalledTimes(1); - expect(listeners.bazOnHost).toHaveBeenCalledWith(jasmine.objectContaining({ - type: 'baz', - detail: false, - })); - - expect(listeners['q-u-xOnNgElement']).toHaveBeenCalledTimes(1); - expect(listeners['q-u-xOnNgElement']).toHaveBeenCalledWith(jasmine.objectContaining({ - type: 'q-u-x', - detail: {qux: true}, - })); - expect(listeners['q-u-xOnHost']).toHaveBeenCalledTimes(1); - expect(listeners['q-u-xOnHost']).toHaveBeenCalledWith(jasmine.objectContaining({ - type: 'q-u-x', - detail: {qux: true}, - })); - }); - - it('should run output event listeners outside the Angular zone', () => { - const expectOutsideNgZone = () => expect(NgZone.isInAngularZone()).toBe(false); - const listeners = { - bazOnNgElement: - jasmine.createSpy('bazOnNgElement').and.callFake(expectOutsideNgZone), - bazOnHost: jasmine.createSpy('bazOnHost').and.callFake(expectOutsideNgZone), - }; - - e.addEventListener('baz', listeners.bazOnNgElement); - e.connectedCallback(); - host.addEventListener('baz', listeners.bazOnHost); - - const ngZone = moduleRef.injector.get(NgZone); - ngZone.run(() => e.componentRef !.instance.baz.emit(true)); - - expect(listeners.bazOnNgElement).toHaveBeenCalledTimes(1); - expect(listeners.bazOnHost).toHaveBeenCalledTimes(1); - }); - - it('should trigger change detection', () => { - expect(detectChangesSpy).not.toHaveBeenCalled(); - - e.connectedCallback(); - expect(detectChangesSpy).toHaveBeenCalledWith(); - }); - - it('should wire up the component for change detection', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - const expectedContent1 = 'TestComponent|foo(foo)|bar()|baz()|rest()'; - const expectedContent2 = 'TestComponent|foo(foo)|bar(newBar)|baz()|rest()'; - - e.connectedCallback(); - const detectChangesSpy = - spyOn(e.componentRef !.changeDetectorRef, 'detectChanges').and.callThrough(); - - expect(host.textContent).toBe(expectedContent1); - - e.componentRef !.instance.bar = 'newBar'; - appRef.tick(); - - expect(detectChangesSpy).toHaveBeenCalledWith(); - expect(host.textContent).toBe(expectedContent2); - }); - - it('should set `ngElement` on both itself and the host (if not the same)', () => { - expect(e.ngElement).toBeFalsy(); - expect((host as typeof e).ngElement).toBeFalsy(); - - e.connectedCallback(); - - expect(e.ngElement).toBe(e); - expect((host as typeof e).ngElement).toBe(e); - }); - - it('should emit an `onConnected` event', () => { - const onConnectedSpy = jasmine.createSpy('onConnectedSpy'); - - e.onConnected.subscribe(onConnectedSpy); - e.connectedCallback(); - - expect(onConnectedSpy).toHaveBeenCalledTimes(1); - }); - - it('should throw if the host is already upgraded (ignoreUpgraded: false)', () => { - (host as typeof e).ngElement = { - componentRef: {componentType: class FooComponent{}} - } as any; - const errorMessage = - `Upgrading '${nodeName}' element to component 'TestComponent' is not allowed, ` + - 'because the element is already upgraded to component \'FooComponent\'.'; - - expect(() => e.connectedCallback()).toThrowError(errorMessage); - expect(e.componentRef).toBeNull(); - - expect(() => e.connectedCallback(false)).toThrowError(errorMessage); - expect(e.componentRef).toBeNull(); - - expect(detectChangesSpy).not.toHaveBeenCalled(); - }); - - it('should do nothing if the host is already upgraded (ignoreUpgraded: true)', () => { - (host as typeof e).ngElement = {} as any; - - expect(() => e.connectedCallback(true)).not.toThrow(); - expect(e.componentRef).toBeNull(); - - expect(detectChangesSpy).not.toHaveBeenCalled(); - }); - - it('should do nothing if already connected', () => { - e.connectedCallback(); - - const componentRef = e.componentRef; - detectChangesSpy.calls.reset(); - - e.connectedCallback(); - - expect(e.componentRef).toBe(componentRef); - expect(detectChangesSpy).not.toHaveBeenCalled(); - }); - - it('should cancel a scheduled destruction (and do nothing)', () => { - const onDisconnectedSpy = jasmine.createSpy('onDisconnected'); - - e.onDisconnected.subscribe(onDisconnectedSpy); - e.connectedCallback(); - e.disconnectedCallback(); - e.disconnectedCallback(); - - const componentRef = e.componentRef; - detectChangesSpy.calls.reset(); - - mockScheduler.tick(DESTROY_DELAY - 1); - e.connectedCallback(); - mockScheduler.tick(DESTROY_DELAY); - - expect(e.componentRef).toBe(componentRef); - expect(detectChangesSpy).not.toHaveBeenCalled(); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - }); - - it('should throw when disconnected', () => { - const errorMessage = - 'Calling \'connectedCallback()\' on disconnected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - disconnectSync(e); - detectChangesSpy.calls.reset(); - - expect(() => e.connectedCallback()).toThrowError(errorMessage); - expect(e.ngElement).toBeNull(); - expect(detectChangesSpy).not.toHaveBeenCalled(); - }); - }); - - describe('detach()', () => { - it('should delegate to `disconnectedCallback()`', () => { - const disconnectedCallbackSpy = spyOn(e, 'disconnectedCallback'); - - expect(disconnectedCallbackSpy).not.toHaveBeenCalled(); - - e.detach(); - expect(disconnectedCallbackSpy).toHaveBeenCalledWith(); - }); - }); - - describe('detectChanges()', () => { - it('should throw when unconnected', () => { - const errorMessage = - 'Calling \'detectChanges()\' on unconnected component \'TestComponent\' is not allowed.'; - expect(() => e.detectChanges()).toThrowError(errorMessage); - }); - - it('should allow scheduling more change detection', () => { - e.connectedCallback(); - - const detectChangesSpy = spyOn(e, 'detectChanges').and.callThrough(); - - e.markDirty(); - e.markDirty(); - mockScheduler.flushBeforeRender(); - - expect(detectChangesSpy).toHaveBeenCalledTimes(1); - - detectChangesSpy.calls.reset(); - e.markDirty(); - e.detectChanges(); - e.markDirty(); - mockScheduler.flushBeforeRender(); - - expect(detectChangesSpy).toHaveBeenCalledTimes(3); - }); - - it('should call `ngOnChanges()` (if implemented and there are changes)', () => { - e.connectedCallback(); - - const compRef = e.componentRef !; - const ngOnChangesSpy = spyOn(compRef.instance, 'ngOnChanges'); - - e.foo = 'newFoo'; - e.detectChanges(); - - expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); - }); - - it('should call `ngOnChanges()` inside the Angular zone', () => { - e.connectedCallback(); - - const compRef = e.componentRef !; - const ngOnChangesSpy = - spyOn(compRef.instance, 'ngOnChanges') - .and.callFake(() => expect(NgZone.isInAngularZone()).toBe(true)); - - e.foo = 'newFoo'; - e.detectChanges(); - - expect(NgZone.isInAngularZone()).toBe(false); - expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); - }); - - it('should not call `ngOnChanges()` if the component does not implement it', () => { - const ngOnChanges = TestComponent.prototype.ngOnChanges; - - try { - TestComponent.prototype.ngOnChanges = null as any; - e.connectedCallback(); - } finally { - TestComponent.prototype.ngOnChanges = ngOnChanges; - } - - const compRef = e.componentRef !; - const ngOnChangesSpy = spyOn(compRef.instance, 'ngOnChanges'); - - e.foo = 'newFoo'; - e.detectChanges(); - - expect(ngOnChangesSpy).not.toHaveBeenCalled(); - }); - - it('should not call `ngOnChanges()` if there are no changes', () => { - e.connectedCallback(); - - const compRef = e.componentRef !; - const ngOnChangesSpy = spyOn(compRef.instance, 'ngOnChanges'); - - e.detectChanges(); - - expect(ngOnChangesSpy).not.toHaveBeenCalled(); - }); - - it('should reset the "pending changes" flag', () => { - e.connectedCallback(); - - const compRef = e.componentRef !; - const ngOnChangesSpy = spyOn(compRef.instance, 'ngOnChanges'); - - e.foo = 'newFoo'; - e.detectChanges(); - - expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); - - ngOnChangesSpy.calls.reset(); - e.detectChanges(); - - expect(ngOnChangesSpy).not.toHaveBeenCalled(); - }); - - it('should call `detectChanges()` on the component (after `ngOnChanges()`)', () => { - e.connectedCallback(); - - const compRef = e.componentRef !; - const ngOnChangesSpy = - spyOn(compRef.instance, 'ngOnChanges') - .and.callFake(() => expect(cdDetectChangesSpy).not.toHaveBeenCalled()); - const cdDetectChangesSpy = - spyOn(compRef.changeDetectorRef, 'detectChanges').and.callThrough(); - - e.foo = 'newFoo'; - e.detectChanges(); - - expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); - expect(cdDetectChangesSpy).toHaveBeenCalledWith(); - }); - - it('should call `detectChanges()` inside the Angular zone', () => { - e.connectedCallback(); - - const compRef = e.componentRef !; - const originalCdDetectChanges = compRef.changeDetectorRef.detectChanges; - const cdDetectChangesSpy = - spyOn(compRef.changeDetectorRef, 'detectChanges').and.callFake(() => { - expect(NgZone.isInAngularZone()).toBe(true); - originalCdDetectChanges.call(compRef.changeDetectorRef); - }); - - e.foo = 'newFoo'; - e.detectChanges(); - - expect(NgZone.isInAngularZone()).toBe(false); - expect(cdDetectChangesSpy).toHaveBeenCalledWith(); - }); - - it('should do nothing when disconnected', () => { - e.connectedCallback(); - disconnectSync(e); - - const cdDetectChangesSpy = spyOn(e.componentRef !.changeDetectorRef, 'detectChanges'); - - e.detectChanges(); - - expect(cdDetectChangesSpy).not.toHaveBeenCalled(); - }); - }); - - describe('disconnectedCallback()', () => { - it('should throw when unconnected', () => { - const errorMessage = - 'Calling \'disconnectedCallback()\' on unconnected component \'TestComponent\' ' + - 'is not allowed.'; - - expect(() => disconnectSync(e)).toThrowError(errorMessage); - }); - - it('should not be immediately disconnected', () => { - const errorMessage = - 'Calling \'setInputValue()\' on disconnected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - e.disconnectedCallback(); - - expect(() => e.foo = 'newFoo').not.toThrow(); - expect(e.componentRef !.instance.foo).toBe('newFoo'); - - mockScheduler.tick(DESTROY_DELAY); - - expect(() => e.foo = 'newerFoo').toThrowError(errorMessage); - }); - - it('should do nothing when already disconnected', () => { - e.connectedCallback(); - disconnectSync(e); - - const destroySpy = spyOn(e.componentRef !, 'destroy'); - const onDisconnectedSpy = jasmine.createSpy('onDisconnectedSpy'); - e.onDisconnected.subscribe(onDisconnectedSpy); - - disconnectSync(e); - - expect(destroySpy).not.toHaveBeenCalled(); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - }); - - it('should do nothing when already scheduled for destruction', () => { - e.connectedCallback(); - e.disconnectedCallback(); - - const destroySpy = spyOn(e.componentRef !, 'destroy'); - const onDisconnectedSpy = jasmine.createSpy('onDisconnectedSpy'); - e.onDisconnected.subscribe(onDisconnectedSpy); - - mockScheduler.reset(); - disconnectSync(e); - disconnectSync(e); - - expect(destroySpy).not.toHaveBeenCalled(); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - }); - - describe('after some delay', () => { - it('should destroy the component', () => { - e.connectedCallback(); - const destroySpy = spyOn(e.componentRef !, 'destroy'); - - e.disconnectedCallback(); - expect(destroySpy).not.toHaveBeenCalled(); - - mockScheduler.tick(DESTROY_DELAY - 1); - expect(destroySpy).not.toHaveBeenCalled(); - - mockScheduler.tick(1); - expect(destroySpy).toHaveBeenCalledWith(); - }); - - it('should destroy the component inside the Angular zone', () => { - e.connectedCallback(); - const destroySpy = - spyOn(e.componentRef !, 'destroy') - .and.callFake(() => expect(NgZone.isInAngularZone()).toBe(true)); - - disconnectSync(e); - - expect(NgZone.isInAngularZone()).toBe(false); - expect(destroySpy).toHaveBeenCalledWith(); - }); - - it('should stop converting component output emissions to custom events', () => { - const listenerForConnected = jasmine.createSpy('listenerForConnected'); - const listenerForDisconnected = jasmine.createSpy('listenerForDisconnected'); - - e.connectedCallback(); - - const component = e.componentRef !.instance; - const emit = () => { - component.baz.emit(false); - component.qux.emit({qux: true}); - }; - - e.addEventListener('baz', listenerForConnected); - e.addEventListener('q-u-x', listenerForConnected); - host.addEventListener('baz', listenerForConnected); - host.addEventListener('q-u-x', listenerForConnected); - emit(); - - expect(listenerForConnected).toHaveBeenCalledTimes(useItselfAsHost ? 2 : 4); - - listenerForConnected.calls.reset(); - disconnectSync(e); - - e.addEventListener('baz', listenerForDisconnected); - e.addEventListener('q-u-x', listenerForDisconnected); - host.addEventListener('baz', listenerForDisconnected); - host.addEventListener('q-u-x', listenerForDisconnected); - emit(); - - expect(listenerForConnected).not.toHaveBeenCalled(); - expect(listenerForDisconnected).not.toHaveBeenCalled(); - }); - - it('should unset `ngElement` on both itself and the host (if not the same)', () => { - e.connectedCallback(); - disconnectSync(e); - - expect(e.ngElement).toBeNull(); - expect((host as typeof e).ngElement).toBeNull(); - }); - - it('should empty the host', () => { - host.innerHTML = 'not empty'; - - e.connectedCallback(); - disconnectSync(e); - - expect(host.innerHTML).toBe(''); - }); - - it('should emit an `onDisconnected` event', () => { - const onDisconnectedSpy = jasmine.createSpy('onDisconnectedSpy'); - e.onDisconnected.subscribe(onDisconnectedSpy); - - e.connectedCallback(); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - - e.disconnectedCallback(); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - - mockScheduler.tick(DESTROY_DELAY - 1); - expect(onDisconnectedSpy).not.toHaveBeenCalled(); - - mockScheduler.tick(1); - expect(onDisconnectedSpy).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('getHost()', () => { - it('should return the current host (regardless of the lifecycle phase)', () => { - expect(e.getHost()).toBe(host); - - const newHost = document.createElement('new-test-host'); - - e.setHost(newHost); - expect(e.getHost()).toBe(newHost); - - e.connectedCallback(); - expect(e.getHost()).toBe(newHost); - - e.disconnectedCallback(); - expect(e.getHost()).toBe(newHost); - }); - }); - - describe('getInputValue()', () => { - it('should return the corresponding property when unconnected', () => { - expect(e.getInputValue('foo')).toBeUndefined(); - expect(e.getInputValue('bar')).toBeUndefined(); - - e.foo = 'newFoo'; - e.bar = 'newBar'; - - expect(e.getInputValue('foo')).toBe('newFoo'); - expect(e.getInputValue('bar')).toBe('newBar'); - }); - - it('should return the corresponding component property (when connected)', () => { - e.connectedCallback(); - const component = e.componentRef !.instance; - - expect(e.getInputValue('foo')).toBe('foo'); - expect(e.getInputValue('bar')).toBeUndefined(); - - e.foo = 'newFoo'; - e.bar = 'newBar'; - - expect(e.getInputValue('foo')).toBe('newFoo'); - expect(e.getInputValue('bar')).toBe('newBar'); - - component.foo = 'newerFoo'; - component.bar = 'newerBar'; - - expect(e.getInputValue('foo')).toBe('newerFoo'); - expect(e.getInputValue('bar')).toBe('newerBar'); - }); - - it('should throw when disconnected', () => { - const errorMessage = - 'Calling \'getInputValue()\' on disconnected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - disconnectSync(e); - - expect(() => e.getInputValue('foo')).toThrowError(errorMessage); - }); - }); - - describe('markDirty()', () => { - let detectChangesSpy: jasmine.Spy; - - beforeEach(() => { - e.connectedCallback(); - detectChangesSpy = spyOn(e, 'detectChanges'); - }); - - it('should schedule change detection', () => { - e.markDirty(); - expect(detectChangesSpy).not.toHaveBeenCalled(); - - mockScheduler.flushBeforeRender(); - expect(detectChangesSpy).toHaveBeenCalledWith(); - }); - - it('should not schedule change detection if already scheduled', () => { - e.markDirty(); - e.markDirty(); - e.markDirty(); - mockScheduler.flushBeforeRender(); - - expect(detectChangesSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('setHost()', () => { - it('should set the host (when unconnected)', () => { - const newHost = document.createElement('new-test-host'); - e.setHost(newHost); - expect(e.getHost()).toBe(newHost); - }); - - it('should throw when connected', () => { - const errorMessage = - 'Calling \'setHost()\' on connected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - - expect(() => e.setHost({} as any)).toThrowError(errorMessage); - expect(e.getHost()).toBe(host); - }); - - it('should throw when disconnected', () => { - const errorMessage = - 'Calling \'setHost()\' on disconnected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - disconnectSync(e); - - expect(() => e.setHost({} as any)).toThrowError(errorMessage); - expect(e.getHost()).toBe(host); - }); - }); - - describe('setInputValue()', () => { - let markDirtySpy: jasmine.Spy; - - beforeEach(() => markDirtySpy = spyOn(e, 'markDirty')); - - it('should update the corresponding property when unconnected', () => { - expect(e.foo).toBeUndefined(); - expect(e.bar).toBeUndefined(); - - e.setInputValue('foo', 'newFoo'); - e.setInputValue('bar', 'newBar'); - - expect(e.foo).toBe('newFoo'); - expect(e.bar).toBe('newBar'); - - expect(markDirtySpy).not.toHaveBeenCalled(); - }); - - it('should update the corresponding component property (when connected)', () => { - e.connectedCallback(); - const component = e.componentRef !.instance; - - expect(component.foo).toBe('foo'); - expect(component.bar).toBeUndefined(); - - e.setInputValue('foo', 'newFoo'); - e.setInputValue('bar', 'newBar'); - - expect(component.foo).toBe('newFoo'); - expect(component.bar).toBe('newBar'); - }); - - it('should mark as dirty (when connected)', () => { - e.connectedCallback(); - - e.setInputValue('foo', 'newFoo'); - expect(markDirtySpy).toHaveBeenCalledTimes(1); - - e.setInputValue('bar', 'newBar'); - expect(markDirtySpy).toHaveBeenCalledTimes(2); - }); - - it('should not mark as dirty if the new value equals the old one', () => { - e.connectedCallback(); - - e.setInputValue('foo', 'newFoo'); - expect(markDirtySpy).toHaveBeenCalledTimes(1); - - e.setInputValue('foo', 'newFoo'); - expect(markDirtySpy).toHaveBeenCalledTimes(1); - - e.setInputValue('foo', NaN as any); - expect(markDirtySpy).toHaveBeenCalledTimes(2); - - e.setInputValue('foo', NaN as any); - expect(markDirtySpy).toHaveBeenCalledTimes(2); - }); - - it('should record an input change', () => { - e.connectedCallback(); - const component = e.componentRef !.instance; - - e.setInputValue('foo', 'newFoo'); - e.detectChanges(); - - expect(component.lastChanges).toEqual({ - foo: new SimpleChange(undefined, 'newFoo', true), - }); - - e.setInputValue('foo', 'newerFoo'); - e.setInputValue('bar', 'newBar'); - e.detectChanges(); - - expect(component.lastChanges).toEqual({ - foo: new SimpleChange('newFoo', 'newerFoo', false), - bar: new SimpleChange(undefined, 'newBar', true), - }); - }); - - it('should aggregate multiple recorded changes (retaining `firstChange`)', () => { - e.connectedCallback(); - const component = e.componentRef !.instance; - - e.setInputValue('foo', 'newFoo'); - e.setInputValue('foo', 'newerFoo'); - e.setInputValue('foo', 'newestFoo'); - e.detectChanges(); - - expect(component.lastChanges).toEqual({ - foo: new SimpleChange(undefined, 'newestFoo', true), - }); - - e.setInputValue('foo', 'newesterFoo'); - e.setInputValue('foo', 'newestestFoo'); - e.detectChanges(); - - expect(component.lastChanges).toEqual({ - foo: new SimpleChange('newestFoo', 'newestestFoo', false), - }); - }); - - it('should throw when disconnected', () => { - const errorMessage = - 'Calling \'setInputValue()\' on disconnected component \'TestComponent\' is not allowed.'; - - e.connectedCallback(); - disconnectSync(e); - - expect(() => e.setInputValue('foo', 'newFoo')).toThrowError(errorMessage); - }); - }); - - describe('component lifecycle hooks', () => { - let log: string[]; - - beforeEach(() => { - log = []; - - ['AfterContentChecked', 'AfterContentInit', 'AfterViewChecked', 'AfterViewInit', - 'DoCheck', 'OnChanges', 'OnDestroy', 'OnInit', - ] - .forEach( - hook => spyOn(TestComponent.prototype, `ng${hook}` as keyof TestComponent) - .and.callFake( - () => log.push(`${hook}(${NgZone.isInAngularZone()})`))); - }); - - it('should be run on initialization (with initial input changes)', () => { - e.bar = 'newBar'; - e.connectedCallback(); - - expect(log).toEqual([ - // Initialization and local change detection, due to `detectChanges()` (from - // `connectedCallback()`). - 'OnChanges(true)', - 'OnInit(true)', - 'DoCheck(true)', - 'AfterContentInit(true)', - 'AfterContentChecked(true)', - 'AfterViewInit(true)', - 'AfterViewChecked(true)', - // Global change detection, due to `ngZone.run()` (from `connectedCallback()`). - 'DoCheck(true)', - 'AfterContentChecked(true)', - 'AfterViewChecked(true)', - ]); - }); - - it('should be run on initialization (without initial input changes)', () => { - e.connectedCallback(); - - expect(log).toEqual([ - // Initialization and local change detection, due to `detectChanges()` (from - // `connectedCallback()`). - 'OnInit(true)', - 'DoCheck(true)', - 'AfterContentInit(true)', - 'AfterContentChecked(true)', - 'AfterViewInit(true)', - 'AfterViewChecked(true)', - // Global change detection, due to `ngZone.run()` (from `connectedCallback()`). - 'DoCheck(true)', - 'AfterContentChecked(true)', - 'AfterViewChecked(true)', - ]); - }); - - it('should be run on explicit change detection (with input changes)', () => { - e.connectedCallback(); - log.length = 0; - - e.bar = 'newBar'; - e.detectChanges(); - - expect(log).toEqual([ - // Local change detection, due to `detectChanges()`. - 'OnChanges(true)', - 'DoCheck(true)', - 'AfterContentChecked(true)', - 'AfterViewChecked(true)', - // Global change detection, due to `ngZone.run()` (from `detectChanges()`). - 'DoCheck(true)', - 'AfterContentChecked(true)', - 'AfterViewChecked(true)', - ]); - }); - - it('should be run on explicit change detection (without input changes)', () => { - e.connectedCallback(); - log.length = 0; - - e.detectChanges(); - - expect(log).toEqual([ - // Local change detection, due to `detectChanges()`. - 'DoCheck(true)', - 'AfterContentChecked(true)', - 'AfterViewChecked(true)', - // Global change detection, due to `ngZone.run()` (from `detectChanges()`). - 'DoCheck(true)', - 'AfterContentChecked(true)', - 'AfterViewChecked(true)', - ]); - }); - - it('should be run on implicit change detection (with input changes)', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - - e.connectedCallback(); - log.length = 0; - - e.bar = 'newBar'; - appRef.tick(); - - expect(NgZone.isInAngularZone()).toBe(false); - expect(log).toEqual([ - // Since inputs are updated outside of Angular - // `appRef` doesn't know about them (so no `ngOnChanges()`). - 'DoCheck(false)', - 'AfterContentChecked(false)', - 'AfterViewChecked(false)', - ]); - }); - - it('should be run on implicit change detection (without input changes)', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - - e.connectedCallback(); - log.length = 0; - - appRef.tick(); - - expect(NgZone.isInAngularZone()).toBe(false); - expect(log).toEqual([ - 'DoCheck(false)', - 'AfterContentChecked(false)', - 'AfterViewChecked(false)', - ]); - }); - - it('should be run on destruction', () => { - e.connectedCallback(); - log.length = 0; - - disconnectSync(e); - - expect(log).toEqual([ - 'OnDestroy(true)', - ]); - }); - - it('should not be run after destruction', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - - e.connectedCallback(); - disconnectSync(e); - log.length = 0; - - appRef.tick(); - - expect(log).toEqual([]); - }); - }); - }); - }); - }); - - // Helpers - @Component({ - selector: 'test-component-for-nge', - template: 'TestComponent|' + - 'foo({{ foo }})|' + - 'bar({{ bar }})|' + - 'baz()|' + - 'rest()', - }) - class TestComponent implements AfterContentChecked, - AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy, OnInit { - @Input() foo: string = 'foo'; - @Input('b-a-r') bar: string; - createdInNgZone = NgZone.isInAngularZone(); - lastChanges: SimpleChanges; - - @Output() baz = new EventEmitter(); - @Output('q-u-x') qux = new EventEmitter(); - - constructor(@Inject('TEST_VALUE') public testValue: string) {} - - ngOnChanges(changes: SimpleChanges) { this.lastChanges = changes; } - - ngAfterContentChecked() {} - ngAfterContentInit() {} - ngAfterViewChecked() {} - ngAfterViewInit() {} - ngDoCheck() {} - ngOnDestroy() {} - ngOnInit() {} - } - - @NgModule({ - imports: [BrowserModule], - providers: [ - {provide: 'TEST_VALUE', useValue: 'TEST'}, - ], - declarations: [TestComponent], - entryComponents: [TestComponent], - }) - class TestModule { - ngDoBootstrap() {} - } - - class TestNgElement extends NgElementImpl { - static is = 'test-component-for-nge'; - static observedAttributes = ['foo', 'b-a-r']; - - get foo() { return this.getInputValue('foo'); } - set foo(v) { this.setInputValue('foo', v); } - - get bar() { return this.getInputValue('bar'); } - set bar(v) { this.setInputValue('bar', v); } - - constructor() { - const appContext = new NgElementApplicationContext(moduleRef.injector); - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(TestComponent); - - const inputs = factory.inputs.map(({propName, templateName}) => ({ - propName, - attrName: templateName, - })); - const outputs = factory.outputs.map(({propName, templateName}) => ({ - propName, - eventName: templateName, - })); - - super(appContext, factory, inputs, outputs); - } - } - - // The `@webcomponents/custom-elements/src/native-shim.js` polyfill, that we use to enable - // ES2015 classes transpiled to ES5 constructor functions to be used as Custom Elements in - // tests, only works if the elements have been registered with `customElements.define()`. - customElements.define(TestNgElement.is, TestNgElement); - }); -} diff --git a/packages/elements/test/ng-elements_spec.ts b/packages/elements/test/ng-elements_spec.ts deleted file mode 100644 index b5050907e1..0000000000 --- a/packages/elements/test/ng-elements_spec.ts +++ /dev/null @@ -1,552 +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 {ApplicationRef, Component, EventEmitter, Inject, Input, NgModule, NgModuleRef, Output, destroyPlatform} from '@angular/core'; -import {BrowserModule} from '@angular/platform-browser'; -import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {NgElement} from '../src/ng-element'; -import {NgElements} from '../src/ng-elements'; -import {AsyncMockScheduler, installMockScheduler, patchEnv, restoreEnv, supportsCustomElements} from '../testing/index'; - -export function main() { - if (!supportsCustomElements()) { - return; - } - - describe('NgElements', () => { - const DESTROY_DELAY = 10; - let uid = 0; - let mockScheduler: AsyncMockScheduler; - let moduleRef: NgModuleRef; - let e: NgElements; - - beforeAll(() => patchEnv()); - beforeAll(done => { - mockScheduler = installMockScheduler(); - - destroyPlatform(); - platformBrowserDynamic() - .bootstrapModule(TestModule) - .then(ref => moduleRef = ref) - .then(done, done.fail); - }); - - afterAll(() => destroyPlatform()); - afterAll(() => restoreEnv()); - - beforeEach(() => { - mockScheduler.reset(); - - e = new NgElements(moduleRef, [TestComponentX, TestComponentY]); - - // The `@webcomponents/custom-elements/src/native-shim.js` polyfill, that we use to enable - // ES2015 classes transpiled to ES5 constructor functions to be used as Custom Elements in - // tests, only works if the elements have been registered with `customElements.define()`. - // (Using dummy selectors to ensure that the browser does not automatically upgrade the - // inserted elements.) - e.forEach(ctor => customElements.define(`${ctor.is}-${++uid}`, ctor)); - }); - - describe('constructor()', () => { - it('should set the `moduleRef` property', - () => { expect(e.moduleRef.instance).toEqual(jasmine.any(TestModule)); }); - - it('should create an `NgElementConstructor` for each component', () => { - const XConstructor = e.get('test-component-for-nges-x') !; - expect(XConstructor).toEqual(jasmine.any(Function)); - expect(XConstructor.is).toBe('test-component-for-nges-x'); - expect(XConstructor.observedAttributes).toEqual(['x-foo']); - - const YConstructor = e.get('test-component-for-nges-y') !; - expect(YConstructor).toEqual(jasmine.any(Function)); - expect(YConstructor.is).toBe('test-component-for-nges-y'); - expect(YConstructor.observedAttributes).toEqual(['ybar']); - }); - - it('should throw if there are components with the same selector', () => { - const duplicateComponents = [TestComponentX, TestComponentX]; - const errorMessage = - 'Defining an Angular custom element with selector \'test-component-for-nges-x\' is not ' + - 'allowed, because one is already defined.'; - - expect(() => new NgElements(e.moduleRef, duplicateComponents)).toThrowError(errorMessage); - }); - }); - - describe('detachAll()', () => { - let root: Element; - let detachSpies: Map; - - beforeEach(() => { - root = document.createElement('div'); - root.innerHTML = ` -
- , -
    -
  • - -
  • -
  • - -
  • -
  • - - - PROJECTED_CONTENT - - -
  • -
- - - - -
- `; - - e.upgradeAll(root); - - detachSpies = new Map(); - Array.prototype.forEach.call( - root.querySelectorAll('test-component-for-nges-x,test-component-for-nges-y'), - (node: NgElement) => detachSpies.set(node.id, spyOn(node.ngElement !, 'detach'))); - - expect(detachSpies.size).toBe(6); - }); - - it('should detach all upgraded elements in the specified sub-tree', () => { - e.detachAll(root); - detachSpies.forEach(spy => expect(spy).toHaveBeenCalledWith()); - }); - - it('should detach the root node itself (if appropriate)', () => { - const yNode = root.querySelector('#y1') !; - const xNode = root.querySelector('#x3') !; - - e.detachAll(yNode); - - detachSpies.forEach((spy, id) => { - const expectedCallCount = (id === 'y1' || id === 'x3') ? 1 : 0; - expect(spy.calls.count()).toBe(expectedCallCount); - }); - }); - - // For more info on "shadow-including tree order" see: - // https://dom.spec.whatwg.org/#concept-shadow-including-tree-order - it('should detach nodes in reverse "shadow-including tree order"', () => { - const ids: string[] = []; - - detachSpies.forEach((spy, id) => spy.and.callFake(() => ids.push(id))); - e.detachAll(root); - - expect(ids).toEqual(['y2', 'x4', 'x3', 'y1', 'x2', 'x1']); - }); - - it('should ignore already detached elements', () => { - const xNode = root.querySelector('#x1') !; - const ngElement = (xNode as NgElement).ngElement !; - - // Detach node. - ngElement.disconnectedCallback(); - mockScheduler.tick(DESTROY_DELAY); - - // Detach the whole sub-tree (including the already detached node). - e.detachAll(root); - - detachSpies.forEach((spy, id) => { - const expectedCallCount = (id === 'x1') ? 0 : 1; - expect(spy.calls.count()).toBe(expectedCallCount, id); - }); - }); - - it('should detach the whole document if no root node is specified', () => { - e.detachAll(); - detachSpies.forEach(spy => expect(spy).not.toHaveBeenCalled()); - - document.body.appendChild(root); - - e.detachAll(); - detachSpies.forEach(spy => expect(spy).toHaveBeenCalledTimes(1)); - }); - - it('should not run change detection after detaching each component', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - const tickSpy = spyOn(appRef, 'tick'); - - e.detachAll(root); - - expect(tickSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('detectChanges()', () => { - let xElement: NgElement; - let yElement: NgElement; - let xDetectChangesSpy: jasmine.Spy; - let yDetectChangesSpy: jasmine.Spy; - - beforeEach(() => { - const XConstructor = e.get('test-component-for-nges-x') !; - const YConstructor = e.get('test-component-for-nges-y') !; - - xElement = new XConstructor(); - yElement = new YConstructor(); - - xDetectChangesSpy = spyOn(xElement, 'detectChanges'); - yDetectChangesSpy = spyOn(yElement, 'detectChanges'); - }); - - it('should not affect unconnected elements', () => { - e.detectChanges(); - - expect(xDetectChangesSpy).not.toHaveBeenCalled(); - expect(yDetectChangesSpy).not.toHaveBeenCalled(); - }); - - it('should call `detectChanges()` on all connected elements', () => { - xElement.connectedCallback(); - xDetectChangesSpy.calls.reset(); - e.detectChanges(); - - expect(xDetectChangesSpy).toHaveBeenCalledTimes(1); - expect(yDetectChangesSpy).not.toHaveBeenCalled(); - - yElement.connectedCallback(); - yDetectChangesSpy.calls.reset(); - e.detectChanges(); - - expect(xDetectChangesSpy).toHaveBeenCalledTimes(2); - expect(yDetectChangesSpy).toHaveBeenCalledTimes(1); - }); - - it('should not affect disconnected elements', () => { - xElement.connectedCallback(); - yElement.connectedCallback(); - xDetectChangesSpy.calls.reset(); - yDetectChangesSpy.calls.reset(); - e.detectChanges(); - - expect(xDetectChangesSpy).toHaveBeenCalledTimes(1); - expect(yDetectChangesSpy).toHaveBeenCalledTimes(1); - - xElement.disconnectedCallback(); - mockScheduler.tick(DESTROY_DELAY); - e.detectChanges(); - - expect(xDetectChangesSpy).toHaveBeenCalledTimes(1); - expect(yDetectChangesSpy).toHaveBeenCalledTimes(2); - - yElement.disconnectedCallback(); - mockScheduler.tick(DESTROY_DELAY); - e.detectChanges(); - - expect(xDetectChangesSpy).toHaveBeenCalledTimes(1); - expect(yDetectChangesSpy).toHaveBeenCalledTimes(2); - }); - - it('should allow scheduling more change detection', () => { - const detectChangesSpy = spyOn(e, 'detectChanges').and.callThrough(); - - e.markDirty(); - e.markDirty(); - mockScheduler.flushBeforeRender(); - - expect(detectChangesSpy).toHaveBeenCalledTimes(1); - - detectChangesSpy.calls.reset(); - e.markDirty(); - e.detectChanges(); - e.markDirty(); - mockScheduler.flushBeforeRender(); - - expect(detectChangesSpy).toHaveBeenCalledTimes(3); - }); - - it('should not run global change detection after checking each component', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - const tickSpy = spyOn(appRef, 'tick'); - - xElement.connectedCallback(); - yElement.connectedCallback(); - tickSpy.calls.reset(); - - e.detectChanges(); - - expect(tickSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('forEach()', () => { - it('should allow looping through all `NgElementConstructor`s', () => { - const selectors = ['test-component-for-nges-x', 'test-component-for-nges-y']; - const callbackSpy = jasmine.createSpy('callback'); - e.forEach(callbackSpy); - - expect(callbackSpy).toHaveBeenCalledTimes(selectors.length); - selectors.forEach( - selector => expect(callbackSpy) - .toHaveBeenCalledWith(e.get(selector), selector, jasmine.any(Map))); - }); - }); - - describe('get()', () => { - it('should return the `ngElementConstructor` for the specified selector (if any)', () => { - expect(e.get('test-component-for-nges-x')).toEqual(jasmine.any(Function)); - expect(e.get('test-component-for-nges-y')).toEqual(jasmine.any(Function)); - expect(e.get('test-component-for-nges-z')).toBeUndefined(); - }); - }); - - describe('register()', () => { - let defineSpy: jasmine.Spy; - - beforeEach(() => defineSpy = spyOn(window.customElements, 'define')); - - it('should add each `NgElementConstructor` to the `CustomElementRegistry`', () => { - e.register(); - - expect(defineSpy).toHaveBeenCalledTimes(2); - e.forEach((ctor, selector) => expect(defineSpy).toHaveBeenCalledWith(selector, ctor)); - }); - - it('should support specifying a different `CustomElementRegistry`', () => { - const mockDefineSpy = jasmine.createSpy('mockDefine'); - - e.register({ define: mockDefineSpy } as any); - - expect(defineSpy).not.toHaveBeenCalled(); - expect(mockDefineSpy).toHaveBeenCalledTimes(2); - e.forEach((ctor, selector) => expect(mockDefineSpy).toHaveBeenCalledWith(selector, ctor)); - }); - - it('should throw if there is no `CustomElementRegistry`', () => { - const originalDescriptor = Object.getOwnPropertyDescriptor(window, 'customElements'); - const errorMessage = 'Custom Elements are not supported in this environment.'; - - try { - delete window.customElements; - - expect(() => e.register()).toThrowError(errorMessage); - expect(() => e.register(null as any)).toThrowError(errorMessage); - } finally { - Object.defineProperty(window, 'customElements', originalDescriptor); - } - }); - }); - - describe('markDirty()', () => { - let detectChangesSpy: jasmine.Spy; - - beforeEach(() => detectChangesSpy = spyOn(e, 'detectChanges')); - - it('should schedule change detection', () => { - e.markDirty(); - expect(detectChangesSpy).not.toHaveBeenCalled(); - - mockScheduler.flushBeforeRender(); - expect(detectChangesSpy).toHaveBeenCalledWith(); - }); - - it('should not schedule change detection if already scheduled', () => { - e.markDirty(); - e.markDirty(); - e.markDirty(); - mockScheduler.flushBeforeRender(); - - expect(detectChangesSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('upgradeAll()', () => { - const multiTrim = (input: string | null) => input && input.replace(/\s+/g, ''); - let root: Element; - - beforeEach(() => { - root = document.createElement('div'); - root.innerHTML = ` -
- DIV( - , -
    - UL( -
  • - LI(SPAN) -
  • , -
  • - LI() -
  • , -
  • - LI( - - SPAN( - - PROJECTED_CONTENT - - ) - - ) -
  • - ) -
, - - SPAN( - , - - ) - - ) -
- `; - }); - - it('should upgrade all matching elements in the specified sub-tree', () => { - e.upgradeAll(root); - expect(multiTrim(root.textContent)).toBe(multiTrim(` - DIV( - TestComponentX(xFoo)(), - UL( - LI(SPAN), - LI(TestComponentX(xFoo)()), - LI(SPAN(TestComponentY()(TestComponentX(xFoo)(PROJECTED_CONTENT)))) - ), - SPAN( - TestComponentX(newFoo)(), - TestComponentY(newBar)() - ) - ) - `)); - }); - - it('should upgrade the root node itself (if appropriate)', () => { - const yNode = root.querySelector('#y1') !; - e.upgradeAll(yNode); - expect(multiTrim(yNode.textContent)) - .toBe('TestComponentY()(TestComponentX(xFoo)(PROJECTED_CONTENT))'); - }); - - // For more info on "shadow-including tree order" see: - // https://dom.spec.whatwg.org/#concept-shadow-including-tree-order - it('should upgrade nodes in "shadow-including tree order"', () => { - const ids: string[] = []; - - e.forEach( - def => spyOn(def, 'upgrade').and.callFake((node: HTMLElement) => ids.push(node.id))); - e.upgradeAll(root); - - expect(ids).toEqual(['x1', 'x2', 'y1', 'x3', 'x4', 'y2']); - }); - - it('should ignore already upgraded elements (same component)', () => { - const xNode = root.querySelector('#x1') as HTMLElement; - const XConstructor = e.get('test-component-for-nges-x') !; - - // Upgrade node to matching `NgElement`. - expect(XConstructor.is).toBe(xNode.nodeName.toLowerCase()); - const oldNgElement = XConstructor.upgrade(xNode); - const oldComponent = oldNgElement.componentRef !.instance; - - // Upgrade the whole sub-tree (including the already upgraded node). - e.upgradeAll(root); - - const newNgElement = (xNode as NgElement).ngElement !; - const newComponent = newNgElement.componentRef !.instance; - expect(newNgElement).toBe(oldNgElement); - expect(newComponent).toBe(oldComponent); - expect(newComponent).toEqual(jasmine.any(TestComponentX)); - }); - - it('should ignore already upgraded elements (different component)', () => { - const xNode = root.querySelector('#x1') as HTMLElement; - const YConstructor = e.get('test-component-for-nges-y') !; - - // Upgrade node to matching `NgElement`. - expect(YConstructor.is).not.toBe(xNode.nodeName.toLowerCase()); - const oldNgElement = YConstructor.upgrade(xNode); - const oldComponent = oldNgElement.componentRef !.instance; - - // Upgrade the whole sub-tree (including the already upgraded node). - e.upgradeAll(root); - - const newNgElement = (xNode as NgElement).ngElement !; - const newComponent = newNgElement.componentRef !.instance; - expect(newNgElement).toBe(oldNgElement); - expect(newComponent).toBe(oldComponent); - expect(newComponent).toEqual(jasmine.any(TestComponentY)); - }); - - it('should upgrade the whole document if no root node is specified', () => { - const expectedUpgradedTextContent = multiTrim(` - DIV( - TestComponentX(xFoo)(), - UL( - LI(SPAN), - LI(TestComponentX(xFoo)()), - LI(SPAN(TestComponentY()(TestComponentX(xFoo)(PROJECTED_CONTENT)))) - ), - SPAN( - TestComponentX(newFoo)(), - TestComponentY(newBar)() - ) - ) - `); - - e.upgradeAll(); - expect(multiTrim(root.textContent)).not.toBe(expectedUpgradedTextContent); - - document.body.appendChild(root); - - e.upgradeAll(); - expect(multiTrim(root.textContent)).toBe(expectedUpgradedTextContent); - }); - - it('should not run global change detection after upgrading each component', () => { - const appRef = moduleRef.injector.get(ApplicationRef); - const tickSpy = spyOn(appRef, 'tick'); - - e.upgradeAll(root); - - expect(tickSpy).toHaveBeenCalledTimes(1); - }); - }); - - // Helpers - @Component({ - selector: 'test-component-for-nges-x', - template: 'TestComponentX({{ xFoo }})()', - }) - class TestComponentX { - @Input() xFoo: string = 'xFoo'; - @Output() xBaz = new EventEmitter(); - - constructor(@Inject('TEST_VALUE') public testValue: string) {} - } - - @Component({ - selector: 'test-component-for-nges-y', - template: 'TestComponentY({{ yBar }})()', - }) - class TestComponentY { - @Input('ybar') yBar: string; - @Output('yqux') yQux = new EventEmitter(); - - constructor(@Inject('TEST_VALUE') public testValue: string) {} - } - - @NgModule({ - imports: [BrowserModule], - providers: [ - {provide: 'TEST_VALUE', useValue: {value: 'TEST'}}, - ], - declarations: [TestComponentX, TestComponentY], - entryComponents: [TestComponentX, TestComponentY], - }) - class TestModule { - ngDoBootstrap() {} - } - }); -} diff --git a/packages/elements/test/register-as-custom-elements_spec.ts b/packages/elements/test/register-as-custom-elements_spec.ts deleted file mode 100644 index 85dc91715b..0000000000 --- a/packages/elements/test/register-as-custom-elements_spec.ts +++ /dev/null @@ -1,127 +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 {CompilerFactory, Component, NgModule, NgModuleFactory, NgModuleRef, PlatformRef, Type, destroyPlatform} from '@angular/core'; -import {BrowserModule, platformBrowser} from '@angular/platform-browser'; -import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {NgElementImpl} from '../src/ng-element'; -import {registerAsCustomElements} from '../src/register-as-custom-elements'; -import {isFunction} from '../src/utils'; -import {patchEnv, restoreEnv, supportsCustomElements} from '../testing/index'; - -type BootstrapFn = () => Promise>; -type ArgsWithModuleFactory = [PlatformRef, NgModuleFactory]; -type ArgsWithBootstrapFn = [BootstrapFn]; - -export function main() { - if (!supportsCustomElements()) { - return; - } - - describe('registerAsCustomElements()', () => { - const createArgsToRegisterWithModuleFactory = (platformFn: () => PlatformRef) => { - const tempPlatformRef = platformBrowserDynamic(); - const compilerFactory = tempPlatformRef.injector.get(CompilerFactory) as CompilerFactory; - const compiler = compilerFactory.createCompiler([]); - tempPlatformRef.destroy(); - - const platformRef = platformFn(); - const moduleFactory = compiler.compileModuleSync(TestModule); - - return [platformRef, moduleFactory] as ArgsWithModuleFactory; - }; - const createArgsToRegisterWithBootstrapFn = - () => [() => platformBrowserDynamic().bootstrapModule(TestModule)] as - ArgsWithBootstrapFn; - - beforeAll(() => patchEnv()); - afterAll(() => restoreEnv()); - - // Run the tests with both an `NgModuleFactory` and a `bootstrapFn()`. - runTests( - 'with `NgModuleFactory` (on `platformBrowserDynamic`)', - () => createArgsToRegisterWithModuleFactory(platformBrowserDynamic)); - runTests( - 'with `NgModuleFactory` (on `platformBrowser`)', - () => createArgsToRegisterWithModuleFactory(platformBrowser)); - runTests('with `bootstrapFn()`', createArgsToRegisterWithBootstrapFn); - - function runTests( - description: string, createArgs: () => ArgsWithModuleFactory| ArgsWithBootstrapFn) { - describe(description, () => { - const customElementComponents: Type[] = [FooBarComponent, BazQuxComponent]; - const hasBootstrapFn = (arr: any[]): arr is ArgsWithBootstrapFn => isFunction(arr[0]); - let doRegister: () => Promise>; - let defineSpy: jasmine.Spy; - - beforeEach(() => { - destroyPlatform(); - - const args = createArgs(); - doRegister = hasBootstrapFn(args) ? - () => registerAsCustomElements(customElementComponents, args[0]) : - () => registerAsCustomElements(customElementComponents, args[0], args[1]); - - defineSpy = spyOn(customElements, 'define'); - }); - - afterEach(() => destroyPlatform()); - - it('should bootstrap the `NgModule` and return an `NgModuleRef` instance', done => { - doRegister() - .then(ref => expect(ref.instance).toEqual(jasmine.any(TestModule))) - .then(done, done.fail); - }); - - it('should define a custom element for each component', done => { - doRegister() - .then(() => { - expect(defineSpy).toHaveBeenCalledTimes(2); - expect(defineSpy).toHaveBeenCalledWith('foo-bar', jasmine.any(Function)); - expect(defineSpy).toHaveBeenCalledWith('baz-qux', jasmine.any(Function)); - - expect(defineSpy.calls.argsFor(0)[1]).toEqual(jasmine.objectContaining({ - is: 'foo-bar', - observedAttributes: [], - upgrade: jasmine.any(Function), - })); - expect(defineSpy.calls.argsFor(1)[1]).toEqual(jasmine.objectContaining({ - is: 'baz-qux', - observedAttributes: [], - upgrade: jasmine.any(Function), - })); - }) - .then(done, done.fail); - }); - }); - } - }); -} - -@Component({ - selector: 'foo-bar', - template: 'FooBar', -}) -class FooBarComponent { -} - -@Component({ - selector: 'baz-qux', - template: 'BazQux', -}) -class BazQuxComponent { -} - -@NgModule({ - imports: [BrowserModule], - declarations: [FooBarComponent, BazQuxComponent], - entryComponents: [FooBarComponent, BazQuxComponent], -}) -class TestModule { - ngDoBootstrap() {} -} diff --git a/packages/elements/test/utils_spec.ts b/packages/elements/test/utils_spec.ts deleted file mode 100644 index 2a24b453aa..0000000000 --- a/packages/elements/test/utils_spec.ts +++ /dev/null @@ -1,259 +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 {Type} from '@angular/core'; -import {camelToKebabCase, createCustomEvent, getComponentName, isElement, isFunction, kebabToCamelCase, matchesSelector, scheduler, strictEquals, throwError} from '../src/utils'; - -export function main() { - describe('utils', () => { - describe('scheduler', () => { - describe('schedule()', () => { - let setTimeoutSpy: jasmine.Spy; - let clearTimeoutSpy: jasmine.Spy; - - beforeEach(() => { - setTimeoutSpy = spyOn(window, 'setTimeout').and.returnValue(42); - clearTimeoutSpy = spyOn(window, 'clearTimeout'); - }); - - it('should delegate to `window.setTimeout()`', () => { - const cb = () => null; - const delay = 1337; - - scheduler.schedule(cb, delay); - - expect(setTimeoutSpy).toHaveBeenCalledWith(cb, delay); - }); - - it('should return a function for cancelling the scheduled job', () => { - const cancelFn = scheduler.schedule(() => null, 0); - expect(clearTimeoutSpy).not.toHaveBeenCalled(); - - cancelFn(); - expect(clearTimeoutSpy).toHaveBeenCalledWith(42); - }); - }); - - describe('scheduleBeforeRender()', () => { - if (typeof window.requestAnimationFrame === 'undefined') { - const mockCancelFn = () => undefined; - let scheduleSpy: jasmine.Spy; - - beforeEach( - () => scheduleSpy = spyOn(scheduler, 'schedule').and.returnValue(mockCancelFn)); - - it('should delegate to `scheduler.schedule()`', () => { - const cb = () => null; - expect(scheduler.scheduleBeforeRender(cb)).toBe(mockCancelFn); - expect(scheduleSpy).toHaveBeenCalledWith(cb, 16); - }); - } else { - let requestAnimationFrameSpy: jasmine.Spy; - let cancelAnimationFrameSpy: jasmine.Spy; - - beforeEach(() => { - requestAnimationFrameSpy = spyOn(window, 'requestAnimationFrame').and.returnValue(42); - cancelAnimationFrameSpy = spyOn(window, 'cancelAnimationFrame'); - }); - - it('should delegate to `window.requestAnimationFrame()`', () => { - const cb = () => null; - scheduler.scheduleBeforeRender(cb); - expect(requestAnimationFrameSpy).toHaveBeenCalledWith(cb); - }); - - it('should return a function for cancelling the scheduled job', () => { - const cancelFn = scheduler.scheduleBeforeRender(() => null); - expect(cancelAnimationFrameSpy).not.toHaveBeenCalled(); - - cancelFn(); - expect(cancelAnimationFrameSpy).toHaveBeenCalledWith(42); - }); - } - }); - }); - - describe('camelToKebabCase()', () => { - it('should convert camel-case to kebab-case', () => { - expect(camelToKebabCase('fooBarBazQux')).toBe('foo-bar-baz-qux'); - expect(camelToKebabCase('foo1Bar2Baz3Qux4')).toBe('foo1-bar2-baz3-qux4'); - }); - - it('should keep existing dashes', - () => { expect(camelToKebabCase('fooBar-baz-Qux')).toBe('foo-bar-baz--qux'); }); - }); - - describe('createCustomEvent()', () => { - it('should create a custom event (with appropriate properties)', () => { - const value = {bar: 'baz'}; - const event = createCustomEvent(document, 'foo', value); - - expect(event).toEqual(jasmine.any(CustomEvent)); - expect(event).toEqual(jasmine.any(Event)); - expect(event.type).toBe('foo'); - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - expect(event.detail).toEqual(value); - }); - - }); - - describe('getComponentName()', () => { - it('should return the component\'s name', () => { - class Foo {} - expect(getComponentName(Foo)).toBe('Foo'); - }); - - it('should return the `overriddenName` (if present)', () => { - class Foo { - static overriddenName = 'Bar'; - } - expect(getComponentName(Foo)).toBe('Bar'); - }); - - it('should return the first line of the stringified component if no name', () => { - const Foo = {toString: () => 'Baz\nQux'}; - expect(getComponentName(Foo as Type)).toBe('Baz'); - }); - }); - - describe('isElement()', () => { - it('should return true for Element nodes', () => { - const elems = [ - document.body, - document.createElement('div'), - document.createElement('option'), - document.documentElement, - ]; - - elems.forEach(n => expect(isElement(n)).toBe(true)); - }); - - it('should return false for non-Element nodes', () => { - const nonElems = [ - document, - document.createAttribute('foo'), - document.createDocumentFragment(), - document.createComment('bar'), - document.createTextNode('baz'), - ]; - - nonElems.forEach(n => expect(isElement(n)).toBe(false)); - }); - }); - - describe('isFunction()', () => { - it('should return true for functions', () => { - const obj = {foo: function() {}, bar: () => null, baz() {}}; - const fns = [ - function(){}, - () => null, - obj.foo, - obj.bar, - obj.baz, - Function, - Date, - ]; - - fns.forEach(v => expect(isFunction(v)).toBe(true)); - }); - - it('should return false for non-functions', () => { - const nonFns = [ - undefined, - null, - true, - 42, - {}, - ]; - - nonFns.forEach(v => expect(isFunction(v)).toBe(false)); - }); - }); - - describe('kebabToCamelCase()', () => { - it('should convert camel-case to kebab-case', () => { - expect(kebabToCamelCase('foo-bar-baz-qux')).toBe('fooBarBazQux'); - expect(kebabToCamelCase('foo1-bar2-baz3-qux4')).toBe('foo1Bar2Baz3Qux4'); - expect(kebabToCamelCase('foo-1-bar-2-baz-3-qux-4')).toBe('foo1Bar2Baz3Qux4'); - }); - - it('should keep uppercase letters', () => { - expect(kebabToCamelCase('foo-barBaz-Qux')).toBe('fooBarBaz-Qux'); - expect(kebabToCamelCase('foo-barBaz--qux')).toBe('fooBarBaz-Qux'); - }); - }); - - describe('matchesSelector()', () => { - let li: HTMLLIElement; - - beforeEach(() => { - const div = document.createElement('div'); - div.innerHTML = ` -
- -
    -
  • -
-
- `; - li = div.querySelector('li') !; - }); - - it('should return whether the element matches the selector', () => { - expect(matchesSelector(li, 'li')).toBe(true); - expect(matchesSelector(li, '.qux')).toBe(true); - expect(matchesSelector(li, '#quxLi')).toBe(true); - expect(matchesSelector(li, '.qux#quxLi:not(.quux)')).toBe(true); - expect(matchesSelector(li, '.bar > #bazUl > li')).toBe(true); - expect(matchesSelector(li, '.bar .baz ~ .baz li')).toBe(true); - - expect(matchesSelector(li, 'ol')).toBe(false); - expect(matchesSelector(li, '.quux')).toBe(false); - expect(matchesSelector(li, '#quuxOl')).toBe(false); - expect(matchesSelector(li, '.qux#quxLi:not(li)')).toBe(false); - expect(matchesSelector(li, '.bar > #bazUl > .quxLi')).toBe(false); - expect(matchesSelector(li, 'div span ul li')).toBe(false); - }); - }); - - describe('strictEquals()', () => { - it('should perform strict equality check', () => { - const values = [ - undefined, - null, - true, - false, - 42, - '42', - () => undefined, - () => undefined, - {}, - {}, - ]; - - values.forEach((v1, i) => { - values.forEach((v2, j) => { expect(strictEquals(v1, v2)).toBe(i === j); }); - }); - }); - - it('should consider two `NaN` values equals', () => { - expect(strictEquals(NaN, NaN)).toBe(true); - expect(strictEquals(NaN, 'foo')).toBe(false); - expect(strictEquals(NaN, 42)).toBe(false); - expect(strictEquals(NaN, null)).toBe(false); - expect(strictEquals(NaN, undefined)).toBe(false); - }); - }); - - describe('throwError()', () => { - it('should throw an error based on the specified message', - () => { expect(() => throwError('Test')).toThrowError('Test'); }); - }); - }); -} diff --git a/packages/elements/testing/index.ts b/packages/elements/testing/index.ts deleted file mode 100644 index ac4794ea10..0000000000 --- a/packages/elements/testing/index.ts +++ /dev/null @@ -1,115 +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 {scheduler} from '../src/utils'; - -export interface MockScheduler { - schedule: (typeof scheduler)['schedule']; - scheduleBeforeRender: (typeof scheduler)['scheduleBeforeRender']; -} - -export class AsyncMockScheduler implements MockScheduler { - private uid = 0; - private pendingBeforeRenderCallbacks: ({id: number, cb: () => void})[] = []; - private pendingDelayedCallbacks: ({id: number, cb: () => void, delay: number})[] = []; - - flushBeforeRender(): void { - while (this.pendingBeforeRenderCallbacks.length) { - const cb = this.pendingBeforeRenderCallbacks.shift() !.cb; - cb(); - } - } - - reset(): void { - this.pendingBeforeRenderCallbacks.length = 0; - this.pendingDelayedCallbacks.length = 0; - } - - schedule(cb: () => void, delay: number): () => void { - const id = ++this.uid; - let idx = this.pendingDelayedCallbacks.length; - - for (let i = this.pendingDelayedCallbacks.length - 1; i >= 0; --i) { - if (this.pendingDelayedCallbacks[i].delay <= delay) { - idx = i + 1; - break; - } - } - this.pendingDelayedCallbacks.splice(idx, 0, {id, cb, delay}); - - return () => this.remove(id, this.pendingDelayedCallbacks); - } - - scheduleBeforeRender(cb: () => void): () => void { - const id = ++this.uid; - this.pendingBeforeRenderCallbacks.push({id, cb}); - return () => this.remove(id, this.pendingBeforeRenderCallbacks); - } - - tick(ms: number): void { - this.flushBeforeRender(); - - this.pendingDelayedCallbacks.forEach(item => item.delay -= ms); - this.pendingDelayedCallbacks = this.pendingDelayedCallbacks.filter(item => { - if (item.delay <= 0) { - const cb = item.cb; - cb(); - return false; - } - return true; - }); - } - - private remove(id: number, items: {id: number}[]): void { - for (let i = 0, ii = items.length; i < ii; ++i) { - if (items[i].id === id) { - items.splice(i, 1); - break; - } - } - } -} - -export class SyncMockScheduler implements MockScheduler { - schedule(cb: () => void, delay: number): () => void { - cb(); - return () => undefined; - } - - scheduleBeforeRender(cb: () => void): () => void { - cb(); - return () => undefined; - } -} - -export function installMockScheduler(isSync?: false): AsyncMockScheduler; -export function installMockScheduler(isSync: true): SyncMockScheduler; -export function installMockScheduler(isSync?: boolean): AsyncMockScheduler|SyncMockScheduler { - const mockScheduler = isSync ? new SyncMockScheduler() : new AsyncMockScheduler(); - - Object.keys(scheduler).forEach((method: keyof typeof scheduler) => { - spyOn(scheduler, method).and.callFake(mockScheduler[method].bind(mockScheduler)); - }); - - return mockScheduler; -} - -export function patchEnv() { - // This helper function is defined in `test-main.js`. See there for more details. - (window as any).$$patchInnerHtmlProp(); -} - -export function restoreEnv() { - // This helper function is defined in `test-main.js`. See there for more details. - (window as any).$$restoreInnerHtmlProp(); -} - -export function supportsCustomElements() { - // The browser does not natively support custom elements and is not polyfillable. - return typeof customElements !== 'undefined'; -} diff --git a/packages/elements/tsconfig-build.json b/packages/elements/tsconfig-build.json deleted file mode 100644 index 4214dac385..0000000000 --- a/packages/elements/tsconfig-build.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "../tsconfig-build.json", - - "compilerOptions": { - "baseUrl": ".", - "rootDir": ".", - "paths": { - "@angular/common": ["../../dist/packages/common"], - "@angular/core": ["../../dist/packages/core"], - "@angular/platform-browser": ["../../dist/packages/platform-browser"] - }, - "outDir": "../../dist/packages/elements" - }, - - "files": [ - "public_api.ts", - "../../node_modules/zone.js/dist/zone.js.d.ts" - ], - - "angularCompilerOptions": { - "annotateForClosureCompiler": true, - "strictMetadataEmit": false, - "skipTemplateCodegen": true, - "flatModuleOutFile": "elements.js", - "flatModuleId": "@angular/elements" - } -} diff --git a/test-main.js b/test-main.js index a4208e1bfd..6ce3887dab 100644 --- a/test-main.js +++ b/test-main.js @@ -62,40 +62,25 @@ System.config({ '@angular/platform-server': {main: 'index.js', defaultExtension: 'js'}, '@angular/platform-webworker': {main: 'index.js', defaultExtension: 'js'}, '@angular/platform-webworker-dynamic': {main: 'index.js', defaultExtension: 'js'}, - '@angular/elements': {main: 'index.js', defaultExtension: 'js'}, } }); -// Load browser-specific CustomElement polyfills, set up the test injector, import all the specs, -// execute their `main()` method and kick off Karma (Jasmine). -Promise - .resolve() - - // Load browser-specific polyfills for custom elements. - .then(function() { return loadCustomElementsPolyfills(); }) - - // Load necessary testing packages. - .then(function() { - return Promise.all([ - System.import('@angular/core/testing'), - System.import('@angular/platform-browser-dynamic/testing'), - System.import('@angular/platform-browser/animations') - ]); +// Set up the test injector, then import all the specs, execute their `main()` +// method and kick off Karma (Jasmine). +System.import('@angular/core/testing') + .then(function(coreTesting) { + return Promise + .all([ + System.import('@angular/platform-browser-dynamic/testing'), + System.import('@angular/platform-browser/animations') + ]) + .then(function(mods) { + coreTesting.TestBed.initTestEnvironment( + [mods[0].BrowserDynamicTestingModule, mods[1].NoopAnimationsModule], + mods[0].platformBrowserDynamicTesting()); + }); }) - - // Set up the test injector. - .then(function(mods) { - var coreTesting = mods[0]; - var pbdTesting = mods[1]; - var pbAnimations = mods[2]; - - coreTesting.TestBed.initTestEnvironment( - [pbdTesting.BrowserDynamicTestingModule, pbAnimations.NoopAnimationsModule], - pbdTesting.platformBrowserDynamicTesting()); - }) - - // Import all the specs and execute their `main()` method. .then(function() { return Promise.all(Object .keys(window.__karma__.files) // All files served by Karma. @@ -112,106 +97,9 @@ Promise }); })); }) - - // Kick off karma (Jasmine). .then(function() { __karma__.start(); }, function(error) { console.error(error); }); -function loadCustomElementsPolyfills() { - var loadedPromise = Promise.resolve(); - - // The custom elements polyfill relies on `MutationObserver`. - if (!window.MutationObserver) { - loadedPromise = - loadedPromise - .then(function() { return System.import('node_modules/mutation-observer/index.js'); }) - .then(function(MutationObserver) { window.MutationObserver = MutationObserver; }); - } - - // The custom elements polyfill relies on `Object.setPrototypeOf()`. - if (!Object.setPrototypeOf) { - var getDescriptor = function getDescriptor(obj, prop) { - var descriptor; - while (obj && !descriptor) { - descriptor = Object.getOwnPropertyDescriptor(obj, prop); - obj = Object.getPrototypeOf(obj); - } - return descriptor || {}; - }; - var setPrototypeOf = function setPrototypeOf(obj, proto) { - for (var prop in proto) { - if (!obj.hasOwnProperty(prop)) { - Object.defineProperty(obj, prop, getDescriptor(proto, prop)); - } - } - return obj; - }; - - Object.defineProperty(setPrototypeOf, '$$shimmed', {value: true}); - Object.setPrototypeOf = setPrototypeOf; - } - - // The custom elements polyfill will patch `(HTML)Element` properties, including `innerHTML`: - // https://github.com/webcomponents/custom-elements/blob/32f043c3a5e5fc3e035342c0ef10c6786fa416d7/src/Patch/Element.js#L28-L78 - // The patched `innerHTML` setter will try to traverse the DOM (via `nextSibling`), which leads to - // infinite loops when testing `HtmlSanitizer` with cloberred elements on browsers that do not - // support the `