diff --git a/.circleci/config.yml b/.circleci/config.yml index a98c188f04e7..8645548431c2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -614,9 +614,9 @@ jobs: //packages/common/locales # Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`. - mkdir -p dist/all/@angular/core/test/reflection/ + mkdir -p dist/legacy-test-out/core/test/reflection/ cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \ - dist/all/@angular/core/test/reflection/es5_downleveled_inheritance_fixture.js + dist/legacy-test-out/core/test/reflection/es5_downleveled_inheritance_fixture.js # Locale files are needed for i18n tests running within Saucelabs. These are added # directly as sources so that the TypeScript compilation of `/packages/tsconfig.json` # can succeed. Note that the base locale and currencies files are checked-in, so @@ -624,11 +624,10 @@ jobs: mkdir -p packages/common/locales/extra cp dist/bin/packages/common/locales/*.ts packages/common/locales cp dist/bin/packages/common/locales/extra/*.ts packages/common/locales/extra - # add module umd tsc compile option so the test can work - # properly in the legacy browsers - - run: yarn tsc -p packages/tsconfig-legacy-saucelabs.json --module UMD - - run: yarn tsc -p modules --module UMD + # Build ZoneJS so that it can be loaded globally in the Karma tests. - run: yarn bazel build //packages/zone.js:npm_package + # Build the bundle for all tests to run within Saucelabs + - run: node tools/legacy-saucelabs/build-saucelabs-test-bundle.mjs - run: # Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready. name: Waiting for Saucelabs tunnel to connect @@ -636,8 +635,7 @@ jobs: - run: name: Running tests on Saucelabs. command: | - browsers=$(node -e 'console.log(require("./browser-providers.conf").sauceAliases.CI_REQUIRED.join(","))') - yarn karma start ./karma-js.conf.js --single-run --browsers=${browsers} + KARMA_WEB_TEST_MODE=SL_REQUIRED yarn karma start ./karma-js.conf.js --single-run - run: name: Stop Saucelabs tunnel service command: ./tools/saucelabs/sauce-service.sh stop diff --git a/.pullapprove.yml b/.pullapprove.yml index 648739d8cb07..55498a1847fa 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -1196,6 +1196,7 @@ groups: 'tools/circular_dependency_test/**', 'tools/contributing-stats/**', 'tools/gulp-tasks/**', + 'tools/legacy-saucelabs/**', 'tools/npm/**', 'tools/npm_integration_test/**', 'tools/rxjs/**', diff --git a/browser-providers.conf.js b/browser-providers.conf.js index 4b3c95b2b53a..d8dc2b5e7824 100644 --- a/browser-providers.conf.js +++ b/browser-providers.conf.js @@ -10,98 +10,36 @@ // If the target is set to null, then the browser is not run anywhere during CI. // If a category becomes empty (e.g. BS and required), then the corresponding job must be commented // out in the CI configuration. -var CIconfiguration = { - // Chrome and Firefox run as part of the Bazel browser tests, so we do not run them as - // part of the legacy Saucelabs tests. - 'Chrome': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, - 'Firefox': {unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, - // Set ESR as a not required browser as it fails for Ivy acceptance tests. - 'FirefoxESR': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - // Disabled because using the "beta" channel of Chrome can cause non-deterministic CI results. - // e.g. a new chrome beta version has been released, but the Saucelabs selenium server does - // not provide a chromedriver version that is compatible with the new beta. - 'ChromeBeta': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: false}}, - 'ChromeDev': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - // FirefoxBeta and FirefoxDev should be target:'BS' or target:'SL', and required:true - // Currently deactivated due to https://github.com/angular/angular/issues/7560 - 'FirefoxBeta': {unitTest: {target: null, required: true}, e2e: {target: null, required: false}}, - 'FirefoxDev': {unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, - 'Edge': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - 'Android10': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, - 'Android11': {unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, - 'Safari12': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - 'Safari13': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - 'iOS12': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - 'iOS13': {unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, - 'WindowsPhone': {unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}} +const config = { + 'Android10': {unitTest: {target: 'SL', required: true}}, + 'Android11': {unitTest: {target: 'SL', required: true}}, }; -var customLaunchers = { - 'DartiumWithWebPlatform': - {base: 'Dartium', flags: ['--enable-experimental-web-platform-features']}, - 'ChromeNoSandbox': {base: 'Chrome', flags: ['--no-sandbox']}, - 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '81'}, - 'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'}, - 'SL_CHROMEDEV': {base: 'SauceLabs', browserName: 'chrome', version: 'dev'}, - 'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '76'}, - // Firefox 68 is the current ESR vesion - 'SL_FIREFOXESR': {base: 'SauceLabs', browserName: 'firefox', version: '68'}, - 'SL_FIREFOXBETA': - {base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'beta'}, - 'SL_FIREFOXDEV': - {base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'dev'}, - 'SL_SAFARI12': - {base: 'SauceLabs', browserName: 'safari', platform: 'macOS 10.13', version: '12.1'}, - 'SL_SAFARI13': - {base: 'SauceLabs', browserName: 'safari', platform: 'macOS 10.15', version: '13.0'}, - 'SL_IOS12': { - base: 'SauceLabs', - browserName: 'Safari', - platform: 'iOS', - version: '12.0', - device: 'iPhone 7 Simulator' - }, - 'SL_IOS13': { - base: 'SauceLabs', - browserName: 'Safari', - platform: 'iOS', - version: '13.0', - device: 'iPhone 11 Simulator' - }, - 'SL_EDGE': { - base: 'SauceLabs', - browserName: 'MicrosoftEdge', - platform: 'Windows 10', - version: '14.14393' - }, +/** Whether browsers should be remotely acquired in debug mode. */ +const debugMode = false; + +const customLaunchers = { 'SL_ANDROID10': { base: 'SauceLabs', browserName: 'Chrome', - platform: 'Android', - version: '10.0', - device: 'Android GoogleAPI Emulator' + platformName: 'Android', + platformVersion: '10.0', + deviceName: 'Google Pixel 3a GoogleAPI Emulator', + appiumVersion: '1.20.2', + extendedDebugging: debugMode, }, 'SL_ANDROID11': { base: 'SauceLabs', browserName: 'Chrome', - platform: 'Android', - version: '11.0', - device: 'Android GoogleAPI Emulator' + platformName: 'Android', + platformVersion: '11.0', + deviceName: 'Google Pixel 3a GoogleAPI Emulator', + appiumVersion: '1.20.2', + extendedDebugging: debugMode, }, }; -var sauceAliases = { - 'ALL': Object.keys(customLaunchers).filter(function(item) { - return customLaunchers[item].base == 'SauceLabs'; - }), - 'DESKTOP': ['SL_CHROME', 'SL_FIREFOX', 'SL_EDGE', 'SL_SAFARI12', 'SL_SAFARI13', 'SL_FIREFOXESR'], - 'MOBILE': ['SL_ANDROID10', 'SL_ANDROID11', 'SL_IOS12', 'SL_IOS13'], - 'ANDROID': ['SL_ANDROID10', 'SL_ANDROID11'], - 'FIREFOX': ['SL_FIREFOXESR'], - 'IOS': ['SL_IOS12', 'SL_IOS13'], - 'SAFARI': ['SL_SAFARI12', 'SL_SAFARI13'], - 'BETA': ['SL_CHROMEBETA', 'SL_FIREFOXBETA'], - 'DEV': ['SL_CHROMEDEV', 'SL_FIREFOXDEV'], +const sauceAliases = { 'CI_REQUIRED': buildConfiguration('unitTest', 'SL', true), 'CI_OPTIONAL': buildConfiguration('unitTest', 'SL', false) }; @@ -112,9 +50,9 @@ module.exports = { }; function buildConfiguration(type, target, required) { - return Object.keys(CIconfiguration) + return Object.keys(config) .filter((item) => { - var conf = CIconfiguration[item][type]; + const conf = config[item][type]; return conf.required === required && conf.target === target; }) .map((item) => target + '_' + item.toUpperCase()); diff --git a/karma-js.conf.js b/karma-js.conf.js index 3ca73627a2de..98588131ce58 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -10,8 +10,6 @@ const browserProvidersConf = require('./browser-providers.conf'); const {generateSeed} = require('./tools/jasmine-seed-generator'); const {hostname} = require('os'); -// Karma configuration -// Generated on Thu Sep 25 2014 11:52:02 GMT-0700 (PDT) module.exports = function(config) { const conf = { frameworks: ['jasmine'], @@ -21,14 +19,10 @@ module.exports = function(config) { random: true, seed: generateSeed('karma-js.conf'), }, - captureConsole: process.env.CI ? false : true, + captureConsole: true, }, files: [ - // Sources and specs. - // Loaded through the System loader, in `test-main.js`. - {pattern: 'dist/all/@angular/**/*.js', included: false, watched: true}, - // Serve AngularJS for `ngUpgrade` testing. {pattern: 'node_modules/angular-1.5/angular?(.min).js', included: false, watched: false}, {pattern: 'node_modules/angular-mocks-1.5/angular-mocks.js', included: false, watched: false}, @@ -48,10 +42,9 @@ module.exports = function(config) { 'dist/bin/packages/zone.js/npm_package/bundles/zone-testing.umd.js', 'dist/bin/packages/zone.js/npm_package/bundles/task-tracking.umd.js', - // Including systemjs because it defines `__eval`, which produces correct stack traces. - 'test-events.js', - 'third_party/shims_for_internal_tests.js', - 'node_modules/systemjs/dist/system.src.js', + // Static test assets. + {pattern: 'packages/platform-browser/test/static_assets/**/*', included: false}, + {pattern: 'packages/platform-browser/test/browser/static_assets/**/*', included: false}, // Serve polyfills necessary for testing the `elements` package. { @@ -60,42 +53,9 @@ module.exports = function(config) { watched: false }, - {pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true}, 'node_modules/reflect-metadata/Reflect.js', - 'tools/build/file2modulename.js', - 'test-main.js', - {pattern: 'dist/all/@angular/empty.*', included: false, watched: false}, - {pattern: 'packages/platform-browser/test/static_assets/**', included: false, watched: false}, - { - pattern: 'packages/platform-browser/test/browser/static_assets/**', - included: false, - watched: false, - }, - ], - exclude: [ - 'dist/all/@angular/_testing_init/**', - 'dist/all/@angular/**/e2e_test/**', - 'dist/all/@angular/**/*node_only_spec.js', - 'dist/all/@angular/benchpress/**', - 'dist/all/@angular/compiler-cli/**', - 'dist/all/@angular/compiler-cli/src/ngtsc/**', - 'dist/all/@angular/compiler-cli/test/compliance/**', - 'dist/all/@angular/compiler-cli/test/ngtsc/**', - 'dist/all/@angular/compiler/test/aot/**', - 'dist/all/@angular/compiler/test/render3/**', - 'dist/all/@angular/core/test/bundling/**', - 'dist/all/@angular/core/test/render3/ivy/**', - 'dist/all/@angular/core/test/render3/jit/**', - 'dist/all/@angular/core/test/render3/perf/**', - 'dist/all/@angular/elements/schematics/**', - 'dist/all/@angular/examples/**/e2e_test/*', - 'dist/all/@angular/language-service/**', - 'dist/all/@angular/localize/**/test/**', - 'dist/all/@angular/localize/schematics/**', - 'dist/all/@angular/router/**/test/**', - 'dist/all/@angular/platform-browser/testing/e2e_util.js', - 'dist/examples/**/e2e_test/**', + 'dist/legacy-test-bundle.spec.js', ], customLaunchers: browserProvidersConf.customLaunchers, @@ -133,8 +93,8 @@ module.exports = function(config) { maxDuration: 5400, }, - // Try "websocket" for a faster transmission first. Fallback to "polling" if necessary. - transports: ['websocket', 'polling'], + // Always use `polling` for increased communication stability. + transports: ['polling'], port: 9876, captureTimeout: 180000, @@ -159,6 +119,12 @@ module.exports = function(config) { // Setup the Saucelabs plugin so that it can launch browsers using the proper tunnel. conf.sauceLabs.build = tunnelIdentifier; conf.sauceLabs.tunnelIdentifier = tunnelIdentifier; + + // Patch the `saucelabs` package so that `karma-sauce-launcher` does not attempt downloading + // the test logs from upstream and tries re-uploading them with the Karma enhanced details. + // This slows-down tests/browser restarting and can decrease stability. + // https://github.com/karma-runner/karma-sauce-launcher/blob/59b0c5c877448e064ad56449cd906743721c6b62/src/launcher/launcher.ts#L72-L79. + require('saucelabs').default.prototype.downloadJobAsset = () => Promise.resolve(''); } // For SauceLabs jobs, we set up a domain which resolves to the machine which launched diff --git a/modules/benchmarks/src/old/compiler/compiler_benchmark.ts b/modules/benchmarks/src/old/compiler/compiler_benchmark.ts index 88b135b5609b..4f57f7aa4b4a 100644 --- a/modules/benchmarks/src/old/compiler/compiler_benchmark.ts +++ b/modules/benchmarks/src/old/compiler/compiler_benchmark.ts @@ -8,7 +8,7 @@ import {CompilerConfig, DirectiveResolver} from '@angular/compiler'; import {Component, ComponentResolver, Directive, ViewContainerRef,} from '@angular/core'; -import {ViewMetadata} from '@angular/core/src/metadata/view'; +import {ViewMetadata} from '@angular/core/view'; import {PromiseWrapper} from '@angular/facade/src/async'; import {print, Type} from '@angular/facade/src/lang'; import {bootstrap} from '@angular/platform-browser'; diff --git a/package.json b/package.json index d6e74ec10297..d72555bf16a8 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,8 @@ "inquirer": "^8.0.0", "karma-sauce-launcher": "^4.3.6", "madge": "^5.0.0", - "sauce-connect": "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz", + "multimatch": "^6.0.0", + "sauce-connect": "https://saucelabs.com/downloads/sc-4.7.1-linux.tar.gz", "semver": "^7.3.5", "ts-node": "^10.0.0", "tsec": "0.2.0", @@ -192,6 +193,7 @@ "// 5": "Ensure a single version of webdriver-manager so it is hoisted as the integration tests depend on it being found at ../../node_modules/webdriver-manager", "// 6": "Ensure that `@babel/*` packages match the below versions to avoid conflicts with `types/babel__*`", "// 7": "Ensure that transitive dependencies on `https-proxy-agent` are at minimum v5 as older versions patch NodeJS directly, breaking tools like webdriver which is used by the karma-sauce-launcher as an example.", + "// 8": "Ensure that a single instance of the `saucelabs` package is used. Protractor and the Karma sauce launcher pull this package as dependency. A single instance allows for e.g. easier patching in the Karma config.", "resolutions": { "**/graceful-fs": "4.2.8", "**/webdriver-manager": "12.1.8", @@ -202,6 +204,7 @@ "@babel/template": "7.8.6", "@babel/traverse": "7.8.6", "@babel/types": "7.8.6", - "**/https-proxy-agent": "5.0.0" + "**/https-proxy-agent": "5.0.0", + "**/saucelabs": "4.7.8" } } diff --git a/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts b/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts index cfeb4060d54f..469e8b9b70e8 100644 --- a/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts +++ b/packages/compiler-cli/src/transformers/downlevel_decorators_transform/downlevel_decorators_transform.ts @@ -7,7 +7,9 @@ */ import ts from 'typescript'; + import {Decorator, ReflectionHost} from '../../ngtsc/reflection'; + import {isAliasImportDeclaration, loadIsReferencedAliasDeclarationPatch} from './patch_alias_reference_resolution'; /** @@ -432,9 +434,9 @@ export function getDownlevelDecoratorsTransform( ctor = ts.visitEachChild(ctor, decoratorDownlevelVisitor, context); const newParameters: ts.ParameterDeclaration[] = []; - const oldParameters = - ts.visitParameterList(ctor.parameters, decoratorDownlevelVisitor, context); + const oldParameters = ctor.parameters; const parametersInfo: ParameterDecorationInfo[] = []; + for (const param of oldParameters) { const decoratorsToKeep: ts.Decorator[] = []; const paramInfo: ParameterDecorationInfo = {decorators: [], type: null}; @@ -465,9 +467,8 @@ export function getDownlevelDecoratorsTransform( param.dotDotDotToken, param.name, param.questionToken, param.type, param.initializer); newParameters.push(newParam); } - const updated = ts.updateConstructor( - ctor, ctor.decorators, ctor.modifiers, newParameters, - ts.visitFunctionBody(ctor.body, decoratorDownlevelVisitor, context)); + const updated = + ts.updateConstructor(ctor, ctor.decorators, ctor.modifiers, newParameters, ctor.body); return [updated, parametersInfo]; } diff --git a/packages/compiler-cli/test/BUILD.bazel b/packages/compiler-cli/test/BUILD.bazel index 03cf2fcc09a5..97319801e9ba 100644 --- a/packages/compiler-cli/test/BUILD.bazel +++ b/packages/compiler-cli/test/BUILD.bazel @@ -52,6 +52,28 @@ jasmine_node_test( ], ) +ts_library( + name = "downlevel_decorator_transform_lib", + testonly = True, + srcs = [ + "downlevel_decorators_transform_spec.ts", + ], + deps = [ + ":test_utils", + "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/compiler-cli/src/transformers/downlevel_decorators_transform", + "@npm//typescript", + ], +) + +jasmine_node_test( + name = "downlevel_decorator_transform", + bootstrap = ["//tools/testing:node_es5"], + deps = [ + ":downlevel_decorator_transform_lib", + ], +) + # perform_watch_spec ts_library( name = "perform_watch_lib", diff --git a/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts b/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts new file mode 100644 index 000000000000..ef06052ee7c0 --- /dev/null +++ b/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts @@ -0,0 +1,911 @@ +/** + * @license + * Copyright Google LLC 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 ts from 'typescript'; + +import {TypeScriptReflectionHost} from '../src/ngtsc/reflection'; +import {getDownlevelDecoratorsTransform} from '../src/transformers/downlevel_decorators_transform/index'; + +import {MockAotContext, MockCompilerHost} from './mocks'; + +const TEST_FILE_INPUT = '/test.ts'; +const TEST_FILE_OUTPUT = `/test.js`; +const TEST_FILE_DTS_OUTPUT = `/test.d.ts`; + +describe('downlevel decorator transform', () => { + let host: MockCompilerHost; + let context: MockAotContext; + let diagnostics: ts.Diagnostic[]; + let isClosureEnabled: boolean; + let skipClassDecorators: boolean; + + beforeEach(() => { + diagnostics = []; + context = new MockAotContext('/', { + 'dom_globals.d.ts': ` + declare class HTMLElement {}; + declare class Document {}; + ` + }); + host = new MockCompilerHost(context); + isClosureEnabled = false; + skipClassDecorators = false; + }); + + function transform( + contents: string, compilerOptions: ts.CompilerOptions = {}, + preTransformers: ts.TransformerFactory[] = []) { + context.writeFile(TEST_FILE_INPUT, contents); + const program = ts.createProgram( + [TEST_FILE_INPUT, '/dom_globals.d.ts'], { + module: ts.ModuleKind.CommonJS, + importHelpers: true, + lib: ['dom', 'es2015'], + target: ts.ScriptTarget.ES2017, + declaration: true, + experimentalDecorators: true, + emitDecoratorMetadata: false, + ...compilerOptions + }, + host); + const testFile = program.getSourceFile(TEST_FILE_INPUT); + const typeChecker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + const transformers: ts.CustomTransformers = { + before: [ + ...preTransformers, + getDownlevelDecoratorsTransform( + program.getTypeChecker(), reflectionHost, diagnostics, + /* isCore */ false, isClosureEnabled, skipClassDecorators) + ] + }; + let output: string|null = null; + let dtsOutput: string|null = null; + const emitResult = program.emit( + testFile, ((fileName, outputText) => { + if (fileName === TEST_FILE_OUTPUT) { + output = outputText; + } else if (fileName === TEST_FILE_DTS_OUTPUT) { + dtsOutput = outputText; + } + }), + undefined, undefined, transformers); + diagnostics.push(...emitResult.diagnostics); + expect(output).not.toBeNull(); + return { + output: omitLeadingWhitespace(output!), + dtsOutput: dtsOutput ? omitLeadingWhitespace(dtsOutput) : null + }; + } + + it('should downlevel decorators for @Injectable decorated class', () => { + const {output} = transform(` + import {Injectable} from '@angular/core'; + + export class ClassInject {}; + + @Injectable() + export class MyService { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyService.decorators = [ + { type: core_1.Injectable } + ]; + MyService.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should downlevel decorators for @Directive decorated class', () => { + const {output} = transform(` + import {Directive} from '@angular/core'; + + export class ClassInject {}; + + @Directive() + export class MyDir { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should downlevel decorators for @Component decorated class', () => { + const {output} = transform(` + import {Component} from '@angular/core'; + + export class ClassInject {}; + + @Component({template: 'hello'}) + export class MyComp { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyComp.decorators = [ + { type: core_1.Component, args: [{ template: 'hello' },] } + ]; + MyComp.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should downlevel decorators for @Pipe decorated class', () => { + const {output} = transform(` + import {Pipe} from '@angular/core'; + + export class ClassInject {}; + + @Pipe({selector: 'hello'}) + export class MyPipe { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyPipe.decorators = [ + { type: core_1.Pipe, args: [{ selector: 'hello' },] } + ]; + MyPipe.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should not downlevel non-Angular class decorators', () => { + const {output} = transform(` + @SomeUnknownDecorator() + export class MyClass {} + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyClass = (0, tslib_1.__decorate)([ + SomeUnknownDecorator() + ], MyClass); + `); + expect(output).not.toContain('MyClass.decorators'); + }); + + it('should not downlevel non-Angular class decorators generated by a builder', () => { + const {output} = transform(` + @DecoratorBuilder().customClassDecorator + export class MyClass {} + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyClass = (0, tslib_1.__decorate)([ + DecoratorBuilder().customClassDecorator + ], MyClass); + `); + expect(output).not.toContain('MyClass.decorators'); + }); + + it('should downlevel Angular-decorated class member', () => { + const {output} = transform(` + import {Input} from '@angular/core'; + + export class MyDir { + @Input() disabled: boolean = false; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.propDecorators = { + disabled: [{ type: core_1.Input }] + }; + `); + expect(output).not.toContain('tslib'); + }); + + it('should not downlevel class member with unknown decorator', () => { + const {output} = transform(` + export class MyDir { + @SomeDecorator() disabled: boolean = false; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + (0, tslib_1.__decorate)([ + SomeDecorator() + ], MyDir.prototype, "disabled", void 0); + `); + expect(output).not.toContain('MyClass.propDecorators'); + }); + + // Regression test for a scenario where previously the class within a constructor body + // would be processed twice, where the downleveled class is revisited accidentally and + // caused invalid generation of the `ctorParameters` static class member. + it('should not duplicate constructor parameters for classes part of constructor body', () => { + // The bug with duplicated/invalid generation only surfaces when the actual class + // decorators are preserved and emitted by TypeScript itself. This setting is also + // disabled within the CLI. + skipClassDecorators = true; + + const {output} = transform(` + import {Injectable} from '@angular/core'; + + export class ZoneToken {} + + @Injectable() + export class Wrapper { + constructor(y: ZoneToken) { + @Injectable() + class ShouldBeProcessed { + constructor(x: ZoneToken) {} + } + } + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + let Wrapper = class Wrapper { + constructor(y) { + let ShouldBeProcessed = class ShouldBeProcessed { + constructor(x) { } + }; + ShouldBeProcessed.ctorParameters = () => [ + { type: ZoneToken } + ]; + ShouldBeProcessed = (0, tslib_1.__decorate)([ + (0, core_1.Injectable)() + ], ShouldBeProcessed); + } + };`); + }); + + // Angular is not concerned with type information for decorated class members. Instead, + // the type is omitted. This also helps with server side rendering as DOM globals which + // are used as types, do not load at runtime. https://github.com/angular/angular/issues/30586. + it('should downlevel Angular-decorated class member but not preserve type', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output} = transform(` + import {Input} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + export class MyDir { + @Input() trigger: HTMLElement; + @Input() fromOtherFile: MyOtherClass; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.propDecorators = { + trigger: [{ type: core_1.Input }], + fromOtherFile: [{ type: core_1.Input }] + }; + `); + expect(output).not.toContain('HTMLElement'); + expect(output).not.toContain('MyOtherClass'); + }); + + it('should capture constructor type metadata with `emitDecoratorMetadata` enabled', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + @Directive() + export class MyDir { + constructor(other: MyOtherClass) {} + } + `, + {emitDecoratorMetadata: true}); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('const other_file_1 = require("./other-file");'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: other_file_1.MyOtherClass } + ]; + `); + }); + + it('should capture constructor type metadata with `emitDecoratorMetadata` disabled', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output, dtsOutput} = transform( + ` + import {Directive} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + @Directive() + export class MyDir { + constructor(other: MyOtherClass) {} + } + `, + {emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('const other_file_1 = require("./other-file");'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: other_file_1.MyOtherClass } + ]; + `); + expect(dtsOutput).toContain('import'); + }); + + it('should properly serialize constructor parameter with external qualified name type', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output} = transform(` + import {Directive} from '@angular/core'; + import * as externalFile from './other-file'; + + @Directive() + export class MyDir { + constructor(other: externalFile.MyOtherClass) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('const externalFile = require("./other-file");'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: externalFile.MyOtherClass } + ]; + `); + }); + + it('should properly serialize constructor parameter with local qualified name type', () => { + const {output} = transform(` + import {Directive} from '@angular/core'; + + namespace other { + export class OtherClass {} + }; + + @Directive() + export class MyDir { + constructor(other: other.OtherClass) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('var other;'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: other.OtherClass } + ]; + `); + }); + + it('should properly downlevel constructor parameter decorators', () => { + const {output} = transform(` + import {Inject, Directive, DOCUMENT} from '@angular/core'; + + @Directive() + export class MyDir { + constructor(@Inject(DOCUMENT) document: Document) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: Document, decorators: [{ type: core_1.Inject, args: [core_1.DOCUMENT,] }] } + ]; + `); + }); + + it('should properly downlevel constructor parameters with union type', () => { + const {output} = transform(` + import {Optional, Directive, NgZone} from '@angular/core'; + + @Directive() + export class MyDir { + constructor(@Optional() ngZone: NgZone|null) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: core_1.NgZone, decorators: [{ type: core_1.Optional }] } + ]; + `); + }); + + it('should add @nocollapse if closure compiler is enabled', () => { + isClosureEnabled = true; + const {output} = transform(` + import {Directive} from '@angular/core'; + + export class ClassInject {}; + + @Directive() + export class MyDir { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + /** @type {!Array<{type: !Function, args: (undefined|!Array)}>} */ + MyDir.decorators = [ + { type: core_1.Directive } + ]; + `); + expect(output).toContain(dedent` + /** + * @type {function(): !Array<(null|{ + * type: ?, + * decorators: (undefined|!Array<{type: !Function, args: (undefined|!Array)}>), + * })>} + * @nocollapse + */ + MyDir.ctorParameters = () => [ + { type: ClassInject } + ]; + `); + expect(output).not.toContain('tslib'); + }); + + it('should not retain unused type imports due to decorator downleveling with ' + + '`emitDecoratorMetadata` enabled.', + () => { + context.writeFile('/external.ts', ` + export class ErrorHandler {} + export class ClassInject {} + `); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {ErrorHandler, ClassInject} from './external'; + + @Directive() + export class MyDir { + private _errorHandler: ErrorHandler; + constructor(v: ClassInject) {} + } + `, + {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).not.toContain('ErrorHandler'); + }); + + it('should not retain unused type imports due to decorator downleveling with ' + + '`emitDecoratorMetadata` disabled', + () => { + context.writeFile('/external.ts', ` + export class ErrorHandler {} + export class ClassInject {} + `); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {ErrorHandler, ClassInject} from './external'; + + @Directive() + export class MyDir { + private _errorHandler: ErrorHandler; + constructor(v: ClassInject) {} + } + `, + {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).not.toContain('ErrorHandler'); + }); + + it('should not generate invalid reference due to conflicting parameter name', () => { + context.writeFile('/external.ts', ` + export class Dep { + greet() {} + } + `); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {Dep} from './external'; + + @Directive() + export class MyDir { + constructor(Dep: Dep) { + Dep.greet(); + } + } + `, + {emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).toContain(`external_1 = require("./external");`); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: external_1.Dep } + ]; + `); + }); + + it('should be able to serialize circular constructor parameter type', () => { + const {output} = transform(` + import {Directive, Optional, Inject, SkipSelf} from '@angular/core'; + + @Directive() + export class MyDir { + constructor(@Optional() @SkipSelf() @Inject(MyDir) parentDir: MyDir|null) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: MyDir, decorators: [{ type: core_1.Optional }, { type: core_1.SkipSelf }, { type: core_1.Inject, args: [MyDir,] }] } + ]; + `); + }); + + it('should create diagnostic if property name is non-serializable', () => { + transform(` + import {Directive, ViewChild, TemplateRef} from '@angular/core'; + + @Directive() + export class MyDir { + @ViewChild(TemplateRef) ['some' + 'name']: TemplateRef|undefined; + } + `); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].messageText as string) + .toBe(`Cannot process decorators for class element with non-analyzable name.`); + }); + + it('should not capture constructor parameter types when not resolving to a value', () => { + context.writeFile('/external.ts', ` + export interface IState {} + export type IOverlay = {hello: true}&IState; + export default interface { + hello: false; + } + export const enum KeyCodes {A, B} + `); + const {output} = transform(` + import {Directive, Inject} from '@angular/core'; + import * as angular from './external'; + import {IOverlay, KeyCodes} from './external'; + import TypeFromDefaultImport from './external'; + + @Directive() + export class MyDir { + constructor(@Inject('$state') param: angular.IState, + @Inject('$overlay') other: IOverlay, + @Inject('$default') default: TypeFromDefaultImport, + @Inject('$keyCodes') keyCodes: KeyCodes) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('external'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] }, + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] }, + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] }, + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$keyCodes',] }] } + ]; + `); + }); + + it('should allow preceding custom transformers to strip decorators', () => { + const stripAllDecoratorsTransform: ts.TransformerFactory = context => { + return (sourceFile: ts.SourceFile) => { + const visitNode = (node: ts.Node): ts.Node => { + if (ts.isClassDeclaration(node) || ts.isClassElement(node)) { + const cloned = ts.getMutableClone(node); + (cloned.decorators as undefined) = undefined; + return cloned; + } + return ts.visitEachChild(node, visitNode, context); + }; + return visitNode(sourceFile) as ts.SourceFile; + }; + }; + + const {output} = transform( + ` + import {Directive} from '@angular/core'; + + export class MyInjectedClass {} + + @Directive() + export class MyDir { + constructor(someToken: MyInjectedClass) {} + } + `, + {}, [stripAllDecoratorsTransform]); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('MyDir.decorators'); + expect(output).not.toContain('MyDir.ctorParameters'); + expect(output).not.toContain('tslib'); + }); + + it('should capture a non-const enum used as a constructor type', () => { + const {output} = transform(` + import {Component} from '@angular/core'; + + export enum Values {A, B}; + + @Component({template: 'hello'}) + export class MyComp { + constructor(v: Values) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyComp.decorators = [ + { type: core_1.Component, args: [{ template: 'hello' },] } + ]; + MyComp.ctorParameters = () => [ + { type: Values } + ];`); + expect(output).not.toContain('tslib'); + }); + + describe('class decorators skipped', () => { + beforeEach(() => skipClassDecorators = true); + + it('should not downlevel Angular class decorators', () => { + const {output} = transform(` + import {Injectable} from '@angular/core'; + + @Injectable() + export class MyService {} + `); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('MyService.decorators'); + expect(output).toContain(dedent` + MyService = (0, tslib_1.__decorate)([ + (0, core_1.Injectable)() + ], MyService); + `); + }); + + it('should downlevel constructor parameters', () => { + const {output} = transform(` + import {Injectable} from '@angular/core'; + + @Injectable() + export class InjectClass {} + + @Injectable() + export class MyService { + constructor(dep: InjectClass) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('MyService.decorators'); + expect(output).toContain('MyService.ctorParameters'); + expect(output).toContain(dedent` + MyService.ctorParameters = () => [ + { type: InjectClass } + ]; + MyService = (0, tslib_1.__decorate)([ + (0, core_1.Injectable)() + ], MyService); + `); + }); + + it('should downlevel constructor parameter decorators', () => { + const {output} = transform(` + import {Injectable, Inject} from '@angular/core'; + + @Injectable() + export class InjectClass {} + + @Injectable() + export class MyService { + constructor(@Inject('test') dep: InjectClass) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('MyService.decorators'); + expect(output).toContain('MyService.ctorParameters'); + expect(output).toContain(dedent` + MyService.ctorParameters = () => [ + { type: InjectClass, decorators: [{ type: core_1.Inject, args: ['test',] }] } + ]; + MyService = (0, tslib_1.__decorate)([ + (0, core_1.Injectable)() + ], MyService); + `); + }); + + it('should downlevel class member Angular decorators', () => { + const {output} = transform(` + import {Injectable, Input} from '@angular/core'; + + export class MyService { + @Input() disabled: boolean; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).toContain(dedent` + MyService.propDecorators = { + disabled: [{ type: core_1.Input }] + }; + `); + }); + }); + + describe('transforming multiple files', () => { + it('should work correctly for multiple files that import distinct declarations', () => { + context.writeFile('foo_service.d.ts', ` + export declare class Foo {}; + `); + context.writeFile('foo.ts', ` + import {Injectable} from '@angular/core'; + import {Foo} from './foo_service'; + + @Injectable() + export class MyService { + constructor(foo: Foo) {} + } + `); + + context.writeFile('bar_service.d.ts', ` + export declare class Bar {}; + `); + context.writeFile('bar.ts', ` + import {Injectable} from '@angular/core'; + import {Bar} from './bar_service'; + + @Injectable() + export class MyService { + constructor(bar: Bar) {} + } + `); + + const {program, transformers} = createProgramWithTransform(['/foo.ts', '/bar.ts']); + program.emit(undefined, undefined, undefined, undefined, transformers); + + expect(context.readFile('/foo.js')).toContain(`import { Foo } from './foo_service';`); + expect(context.readFile('/bar.js')).toContain(`import { Bar } from './bar_service';`); + }); + + it('should not result in a stack overflow for a large number of files', () => { + // The decorators transform used to patch `ts.EmitResolver.isReferencedAliasDeclaration` + // repeatedly for each source file in the program, causing a stack overflow once a large + // number of source files was reached. This test verifies that emit succeeds even when there's + // lots of source files. See https://github.com/angular/angular/issues/40276. + context.writeFile('foo.d.ts', ` + export declare class Foo {}; + `); + + // A somewhat minimal number of source files that used to trigger a stack overflow. + const numberOfTestFiles = 6500; + const files: string[] = []; + for (let i = 0; i < numberOfTestFiles; i++) { + const file = `/${i}.ts`; + files.push(file); + context.writeFile(file, ` + import {Injectable} from '@angular/core'; + import {Foo} from './foo'; + + @Injectable() + export class MyService { + constructor(foo: Foo) {} + } + `); + } + + const {program, transformers} = createProgramWithTransform(files); + + let written = 0; + program.emit(undefined, (fileName, outputText) => { + written++; + + // The below assertion throws an explicit error instead of using a Jasmine expectation, + // as we want to abort on the first failure, if any. This avoids as many as `numberOfFiles` + // expectation failures, which would bloat the test output. + if (!outputText.includes(`import { Foo } from './foo';`)) { + throw new Error(`Transform failed to preserve the import in ${fileName}:\n${outputText}`); + } + }, undefined, undefined, transformers); + expect(written).toBe(numberOfTestFiles); + }); + + function createProgramWithTransform(files: string[]) { + const program = ts.createProgram( + files, { + moduleResolution: ts.ModuleResolutionKind.NodeJs, + importHelpers: true, + lib: [], + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.Latest, + declaration: false, + experimentalDecorators: true, + emitDecoratorMetadata: false, + }, + host); + const typeChecker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + const transformers: ts.CustomTransformers = { + before: [getDownlevelDecoratorsTransform( + program.getTypeChecker(), reflectionHost, diagnostics, + /* isCore */ false, isClosureEnabled, skipClassDecorators)] + }; + return {program, transformers}; + } + }); +}); + +/** Template string function that can be used to dedent a given string literal. */ +export function dedent(strings: TemplateStringsArray, ...values: any[]) { + let joinedString = ''; + for (let i = 0; i < values.length; i++) { + joinedString += `${strings[i]}${values[i]}`; + } + joinedString += strings[strings.length - 1]; + return omitLeadingWhitespace(joinedString); +} + +/** Omits the leading whitespace for each line of the given text. */ +function omitLeadingWhitespace(text: string): string { + return text.replace(/^\s+/gm, ''); +} diff --git a/packages/core/src/render3/di_setup.ts b/packages/core/src/render3/di_setup.ts index 62395d5c453f..23793bb1c04c 100644 --- a/packages/core/src/render3/di_setup.ts +++ b/packages/core/src/render3/di_setup.ts @@ -191,9 +191,14 @@ function registerDestroyHooksIfSupported( tView: TView, provider: Exclude, contextIndex: number, indexInFactory?: number) { const providerIsTypeProvider = isTypeProvider(provider); - if (providerIsTypeProvider || isClassProvider(provider)) { - const prototype = ((provider as ClassProvider).useClass || provider as TypeProvider).prototype; + const providerIsClassProvider = isClassProvider(provider); + + if (providerIsTypeProvider || providerIsClassProvider) { + // Resolve forward references as `useClass` can hold a forward reference. + const classToken = providerIsClassProvider ? resolveForwardRef(provider.useClass) : provider; + const prototype = classToken.prototype; const ngOnDestroy = prototype.ngOnDestroy; + if (ngOnDestroy) { const hooks = tView.destroyHooks || (tView.destroyHooks = []); diff --git a/packages/core/test/acceptance/content_spec.ts b/packages/core/test/acceptance/content_spec.ts index c30171fc2bc8..2b264f4b4050 100644 --- a/packages/core/test/acceptance/content_spec.ts +++ b/packages/core/test/acceptance/content_spec.ts @@ -7,8 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {ChangeDetectorRef, Component, Directive, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; -import {Input} from '@angular/core/src/metadata'; +import {ChangeDetectorRef, Component, Directive, Input, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -322,7 +321,7 @@ describe('projection', () => { @Component( {selector: 'comp', template: ``}) class Comp { - @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; + @ViewChild(TemplateRef, {static: true}) template!: TemplateRef; } @Directive({selector: '[trigger]'}) diff --git a/packages/core/test/acceptance/directive_spec.ts b/packages/core/test/acceptance/directive_spec.ts index 135e92274f17..b2dce9f02419 100644 --- a/packages/core/test/acceptance/directive_spec.ts +++ b/packages/core/test/acceptance/directive_spec.ts @@ -7,8 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Component, Directive, ElementRef, EventEmitter, NgModule, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; -import {Input} from '@angular/core/src/metadata'; +import {Component, Directive, ElementRef, EventEmitter, Input, NgModule, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; diff --git a/packages/core/test/acceptance/discover_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts index f7c0450c312b..6ce5b4b81a32 100644 --- a/packages/core/test/acceptance/discover_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -6,10 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import {CommonModule} from '@angular/common'; -import {Component, Directive, HostBinding, InjectionToken, ViewChild} from '@angular/core'; -import {ChangeDetectionStrategy} from '@angular/core/src/change_detection'; +import {ChangeDetectionStrategy, Component, Directive, HostBinding, InjectionToken, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core'; import {EventEmitter} from '@angular/core/src/event_emitter'; -import {Input, Output, ViewEncapsulation} from '@angular/core/src/metadata'; import {isLView} from '@angular/core/src/render3/interfaces/type_checks'; import {CONTEXT} from '@angular/core/src/render3/interfaces/view'; import {ComponentFixture, TestBed} from '@angular/core/testing'; diff --git a/packages/core/test/acceptance/text_spec.ts b/packages/core/test/acceptance/text_spec.ts index 952006c5a9a2..c08efd47a5e1 100644 --- a/packages/core/test/acceptance/text_spec.ts +++ b/packages/core/test/acceptance/text_spec.ts @@ -127,7 +127,8 @@ describe('text instructions', () => { fixture.detectChanges(); const div = fixture.nativeElement.querySelector('div'); - expect(div.innerHTML).toBe('function foo() { }'); + expect(div.innerHTML).toBe(fixture.componentInstance.test.toString()); + expect(div.innerHTML).toContain('foo'); }); it('should stringify an object using its toString method', () => { diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts index 1baae1ea6bfc..f8ebadd50d91 100644 --- a/packages/core/test/acceptance/view_container_ref_spec.ts +++ b/packages/core/test/acceptance/view_container_ref_spec.ts @@ -8,8 +8,7 @@ import {CommonModule, DOCUMENT} from '@angular/common'; import {computeMsgId} from '@angular/compiler'; -import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, InjectionToken, Injector, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core'; -import {Input} from '@angular/core/src/metadata'; +import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {ComponentFixture, TestBed, TestComponentRenderer} from '@angular/core/testing'; import {clearTranslations, loadTranslations} from '@angular/localize'; diff --git a/packages/core/test/animation/animations_with_web_animations_integration_spec.ts b/packages/core/test/animation/animations_with_web_animations_integration_spec.ts index a4f1f526654b..c38ae02d3a69 100644 --- a/packages/core/test/animation/animations_with_web_animations_integration_spec.ts +++ b/packages/core/test/animation/animations_with_web_animations_integration_spec.ts @@ -249,7 +249,8 @@ describe('animation integration tests using web animations', function() { overflow:hidden; } .list .inner { - line-height:50px; + box-sizing: border-box; + height: 50px; } `], template: ` diff --git a/packages/core/test/debug/debug_node_spec.ts b/packages/core/test/debug/debug_node_spec.ts index f9f4427f4f34..41360e75a6ce 100644 --- a/packages/core/test/debug/debug_node_spec.ts +++ b/packages/core/test/debug/debug_node_spec.ts @@ -8,8 +8,7 @@ import {CommonModule, NgIfContext, ɵgetDOM as getDOM} from '@angular/common'; -import {Component, DebugElement, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; -import {NgZone} from '@angular/core/src/zone'; +import {Component, DebugElement, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NgZone, NO_ERRORS_SCHEMA, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {createMouseEvent, hasClass} from '@angular/platform-browser/testing/src/browser_util'; diff --git a/packages/core/test/directive_lifecycle_integration_spec.ts b/packages/core/test/directive_lifecycle_integration_spec.ts index 1bddcba6c7e0..b9408fae4ef2 100644 --- a/packages/core/test/directive_lifecycle_integration_spec.ts +++ b/packages/core/test/directive_lifecycle_integration_spec.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnInit} from '@angular/core'; -import {Component, Directive} from '@angular/core/src/metadata'; +import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, Directive, DoCheck, OnChanges, OnInit} from '@angular/core'; import {inject, TestBed} from '@angular/core/testing'; import {Log} from '@angular/core/testing/src/testing_internal'; diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 0d7863351db7..75c5afa52cfd 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule, DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; -import {Compiler, ComponentFactory, ComponentRef, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, SkipSelf, ViewChild, ViewRef} from '@angular/core'; +import {Attribute, Compiler, Component, ComponentFactory, ComponentRef, ContentChildren, Directive, EventEmitter, Host, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, NO_ERRORS_SCHEMA, OnDestroy, Output, Pipe, SkipSelf, ViewChild, ViewRef} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; import {ElementRef} from '@angular/core/src/linker/element_ref'; @@ -15,7 +15,6 @@ import {QueryList} from '@angular/core/src/linker/query_list'; import {TemplateRef} from '@angular/core/src/linker/template_ref'; import {ViewContainerRef} from '@angular/core/src/linker/view_container_ref'; import {EmbeddedViewRef} from '@angular/core/src/linker/view_ref'; -import {Attribute, Component, ContentChildren, Directive, HostBinding, HostListener, Input, Output, Pipe} from '@angular/core/src/metadata'; import {fakeAsync, getTestBed, TestBed, tick, waitForAsync} from '@angular/core/testing'; import {createMouseEvent, dispatchEvent, el, isCommentNode} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; diff --git a/packages/core/test/render3/BUILD.bazel b/packages/core/test/render3/BUILD.bazel index d6e8c62023d0..6e28a0f37418 100644 --- a/packages/core/test/render3/BUILD.bazel +++ b/packages/core/test/render3/BUILD.bazel @@ -2,6 +2,10 @@ load("//tools:defaults.bzl", "jasmine_node_test", "karma_web_test_suite", "ts_li package(default_visibility = ["//visibility:public"]) +ES2015_SPECS = [ + "providers_es2015_spec.ts", +] + ts_library( name = "render3_lib", testonly = True, @@ -14,7 +18,7 @@ ts_library( "is_shape_of.ts", "jit_spec.ts", "matchers.ts", - ], + ] + ES2015_SPECS, ), deps = [ ":matchers", @@ -37,6 +41,20 @@ ts_library( ], ) +ts_library( + name = "render3_es2015_test_lib", + testonly = True, + srcs = ES2015_SPECS, + devmode_target = "es2015", + tsconfig = "es2015-tsconfig.json", + deps = [ + ":matchers", + ":render3_lib", + "//packages:types", + "//packages/core", + ], +) + ts_library( name = "matchers", testonly = True, @@ -80,6 +98,7 @@ jasmine_node_test( "//tools/testing:node_es5", ], deps = [ + ":render3_es2015_test_lib", ":render3_node_lib", "//packages/zone.js/lib", ], @@ -88,6 +107,7 @@ jasmine_node_test( karma_web_test_suite( name = "render3_web", deps = [ + ":render3_es2015_test_lib", ":render3_lib", ], ) diff --git a/packages/core/test/render3/es2015-tsconfig.json b/packages/core/test/render3/es2015-tsconfig.json new file mode 100644 index 000000000000..798a4ed7eca8 --- /dev/null +++ b/packages/core/test/render3/es2015-tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "es2015" + } +} diff --git a/packages/core/test/render3/i18n_debug_spec.ts b/packages/core/test/render3/i18n_debug_spec.ts index 5d1f2d7766d6..5ddcf7151fdc 100644 --- a/packages/core/test/render3/i18n_debug_spec.ts +++ b/packages/core/test/render3/i18n_debug_spec.ts @@ -29,6 +29,13 @@ describe('i18n debug', () => { }); it('should print Attribute opCode', () => { + // The `sanitizeFn` is written as actual function, compared to it being an arrow function. + // This is done to make this test less reluctant to build process changes where e.g. an + // arrow function might be transformed to a function declaration in ES5. + const sanitizeFn = function(v: any) { + return v; + }; + expect(i18nUpdateOpCodesToString([ 0b01, 8, 'pre ', -4, @@ -39,11 +46,12 @@ describe('i18n debug', () => { 'pre ', -4, ' in ', -3, ' post', 1 << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, - 'title', (v: any) => v, + 'title', sanitizeFn, ] as unknown as I18nUpdateOpCodes)) .toEqual([ 'if (mask & 0b1) { (lView[1] as Element).setAttribute(\'title\', `pre ${lView[i-4]} in ${lView[i-3]} post`); }', - 'if (mask & 0b10) { (lView[1] as Element).setAttribute(\'title\', (function (v) { return v; })(`pre ${lView[i-4]} in ${lView[i-3]} post`)); }' + `if (mask & 0b10) { (lView[1] as Element).setAttribute('title', (${ + sanitizeFn.toString()})(\`pre $\{lView[i-4]} in $\{lView[i-3]} post\`)); }` ]); }); diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index c26f12478af8..d1a0bfd6f7d5 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -7,14 +7,10 @@ */ import 'reflect-metadata'; -import {ElementRef, QueryList} from '@angular/core'; +import {Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, HostListener, Input, NgModule, Pipe, QueryList, ViewChild, ViewChildren, ɵNgModuleDef as NgModuleDef} from '@angular/core'; import {Injectable} from '@angular/core/src/di/injectable'; import {setCurrentInjector, ɵɵinject} from '@angular/core/src/di/injector_compatibility'; import {ɵɵdefineInjectable, ɵɵInjectorDef} from '@angular/core/src/di/interface/defs'; -import {ContentChild, ContentChildren, ViewChild, ViewChildren} from '@angular/core/src/metadata/di'; -import {Component, Directive, HostBinding, HostListener, Input, Pipe} from '@angular/core/src/metadata/directives'; -import {NgModule} from '@angular/core/src/metadata/ng_module'; -import {NgModuleDef} from '@angular/core/src/metadata/ng_module_def'; import {FactoryFn} from '@angular/core/src/render3/definition_factory'; import {ComponentDef, PipeDef} from '@angular/core/src/render3/interfaces/definition'; diff --git a/packages/core/test/render3/providers_es2015_spec.ts b/packages/core/test/render3/providers_es2015_spec.ts new file mode 100644 index 000000000000..35098c38db5d --- /dev/null +++ b/packages/core/test/render3/providers_es2015_spec.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC 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 {forwardRef, InjectionToken, ɵɵdirectiveInject} from '@angular/core'; + +import {expectProvidersScenario} from './providers_helper'; + +describe('es2015 providers', () => { + abstract class Greeter { + abstract greet: string; + } + + const GREETER = new InjectionToken('greeter'); + + class GreeterClass implements Greeter { + greet = 'Class'; + hasBeenCleanedUp = false; + + ngOnDestroy() { + this.hasBeenCleanedUp = true; + } + } + + it('ClassProvider wrapped in forwardRef', () => { + let greeterInstance: GreeterClass|null = null; + + expectProvidersScenario({ + parent: { + providers: [{provide: GREETER, useClass: forwardRef(() => GreeterClass)}], + componentAssertion: () => { + greeterInstance = ɵɵdirectiveInject(GREETER) as GreeterClass; + expect(greeterInstance.greet).toEqual('Class'); + } + } + }); + + expect(greeterInstance).not.toBeNull(); + expect(greeterInstance!.hasBeenCleanedUp).toBe(true); + }); +}); diff --git a/packages/core/test/render3/providers_helper.ts b/packages/core/test/render3/providers_helper.ts new file mode 100644 index 000000000000..662cbdb11a04 --- /dev/null +++ b/packages/core/test/render3/providers_helper.ts @@ -0,0 +1,178 @@ +/** + * @license + * Copyright Google LLC 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 {InjectorType, Provider, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵProvidersFeature, ɵɵtext} from '@angular/core'; +import {createInjector} from '@angular/core/src/di/r3_injector'; +import {RenderFlags} from '@angular/core/src/render3'; +import {ComponentFixture} from '@angular/core/test/render3/render_util'; + +export interface ComponentTest { + providers?: Provider[]; + viewProviders?: Provider[]; + directiveProviders?: Provider[]; + directive2Providers?: Provider[]; + directiveAssertion?: () => void; + componentAssertion?: () => void; +} + +export function expectProvidersScenario(defs: { + app?: ComponentTest, + parent?: ComponentTest, + viewChild?: ComponentTest, + contentChild?: ComponentTest, + ngModule?: InjectorType, +}): void { + function testComponentInjection(def: ComponentTest|undefined, instance: T): T { + if (def) { + def.componentAssertion && def.componentAssertion(); + } + return instance; + } + + function testDirectiveInjection(def: ComponentTest|undefined, instance: T): T { + if (def) { + def.directiveAssertion && def.directiveAssertion(); + } + return instance; + } + + class ViewChildComponent { + static ɵfac = () => testComponentInjection(defs.viewChild, new ViewChildComponent()); + static ɵcmp = ɵɵdefineComponent({ + type: ViewChildComponent, + selectors: [['view-child']], + decls: 1, + vars: 0, + template: + function(fs: RenderFlags, ctx: ViewChildComponent) { + if (fs & RenderFlags.Create) { + ɵɵtext(0, 'view-child'); + } + }, + features: defs.viewChild && + [ɵɵProvidersFeature(defs.viewChild.providers || [], defs.viewChild.viewProviders || [])] + }); + } + + class ViewChildDirective { + static ɵfac = () => testDirectiveInjection(defs.viewChild, new ViewChildDirective()); + static ɵdir = ɵɵdefineDirective({ + type: ViewChildDirective, + selectors: [['view-child']], + features: defs.viewChild && [ɵɵProvidersFeature(defs.viewChild.directiveProviders || [])], + }); + } + + class ContentChildComponent { + static ɵfac = + () => { + return testComponentInjection(defs.contentChild, new ContentChildComponent()); + } + + static ɵcmp = ɵɵdefineComponent({ + type: ContentChildComponent, + selectors: [['content-child']], + decls: 1, + vars: 0, + template: + function(fs: RenderFlags, ctx: ParentComponent) { + if (fs & RenderFlags.Create) { + ɵɵtext(0, 'content-child'); + } + }, + features: defs.contentChild && + [ɵɵProvidersFeature( + defs.contentChild.providers || [], defs.contentChild.viewProviders || [])], + }); + } + + class ContentChildDirective { + static ɵfac = + () => { + return testDirectiveInjection(defs.contentChild, new ContentChildDirective()); + } + + static ɵdir = ɵɵdefineDirective({ + type: ContentChildDirective, + selectors: [['content-child']], + features: defs.contentChild && + [ɵɵProvidersFeature(defs.contentChild.directiveProviders || [])], + }); + } + + + class ParentComponent { + static ɵfac = () => testComponentInjection(defs.parent, new ParentComponent()); + static ɵcmp = ɵɵdefineComponent({ + type: ParentComponent, + selectors: [['parent']], + decls: 1, + vars: 0, + template: + function(fs: RenderFlags, ctx: ParentComponent) { + if (fs & RenderFlags.Create) { + ɵɵelement(0, 'view-child'); + } + }, + features: defs.parent && + [ɵɵProvidersFeature(defs.parent.providers || [], defs.parent.viewProviders || [])], + directives: [ViewChildComponent, ViewChildDirective] + }); + } + + class ParentDirective { + static ɵfac = () => testDirectiveInjection(defs.parent, new ParentDirective()); + static ɵdir = ɵɵdefineDirective({ + type: ParentDirective, + selectors: [['parent']], + features: defs.parent && [ɵɵProvidersFeature(defs.parent.directiveProviders || [])], + }); + } + + class ParentDirective2 { + static ɵfac = () => testDirectiveInjection(defs.parent, new ParentDirective2()); + static ɵdir = ɵɵdefineDirective({ + type: ParentDirective2, + selectors: [['parent']], + features: defs.parent && [ɵɵProvidersFeature(defs.parent.directive2Providers || [])], + }); + } + + + class App { + static ɵfac = () => testComponentInjection(defs.app, new App()); + static ɵcmp = ɵɵdefineComponent({ + type: App, + selectors: [['app']], + decls: 2, + vars: 0, + template: + function(fs: RenderFlags, ctx: App) { + if (fs & RenderFlags.Create) { + ɵɵelementStart(0, 'parent'); + ɵɵelement(1, 'content-child'); + ɵɵelementEnd(); + } + }, + features: defs.app && + [ɵɵProvidersFeature(defs.app.providers || [], defs.app.viewProviders || [])], + directives: + [ + ParentComponent, ParentDirective2, ParentDirective, ContentChildComponent, + ContentChildDirective + ] + }); + } + + + const fixture = new ComponentFixture( + App, {injector: defs.ngModule ? createInjector(defs.ngModule) : undefined}); + expect(fixture.html).toEqual('view-child'); + + fixture.destroy(); +} diff --git a/packages/core/test/render3/providers_spec.ts b/packages/core/test/render3/providers_spec.ts index 921e2c5b82ab..122f7a44f8ce 100644 --- a/packages/core/test/render3/providers_spec.ts +++ b/packages/core/test/render3/providers_spec.ts @@ -15,6 +15,7 @@ import {NgModuleFactory} from '../../src/render3/ng_module_ref'; import {getInjector} from '../../src/render3/util/discovery_utils'; import {getRendererFactory2} from './imported_renderer2'; +import {expectProvidersScenario} from './providers_helper'; import {ComponentFixture} from './render_util'; const Component: typeof _Component = function(...args: any[]): any { @@ -37,6 +38,11 @@ describe('providers', () => { class GreeterClass implements Greeter { greet = 'Class'; + hasBeenCleanedUp = false; + + ngOnDestroy() { + this.hasBeenCleanedUp = true; + } } class GreeterDeps implements Greeter { @@ -232,14 +238,20 @@ describe('providers', () => { }); it('ClassProvider wrapped in forwardRef', () => { + let greeterInstance: GreeterClass|null = null; + expectProvidersScenario({ parent: { providers: [{provide: GREETER, useClass: forwardRef(() => GreeterClass)}], componentAssertion: () => { - expect(ɵɵdirectiveInject(GREETER).greet).toEqual('Class'); + greeterInstance = ɵɵdirectiveInject(GREETER) as GreeterClass; + expect(greeterInstance.greet).toEqual('Class'); } } }); + + expect(greeterInstance).not.toBeNull(); + expect(greeterInstance!.hasBeenCleanedUp).toBe(true); }); it('ExistingProvider wrapped in forwardRef', () => { @@ -1386,167 +1398,3 @@ describe('providers', () => { }); }); }); - -interface ComponentTest { - providers?: Provider[]; - viewProviders?: Provider[]; - directiveProviders?: Provider[]; - directive2Providers?: Provider[]; - directiveAssertion?: () => void; - componentAssertion?: () => void; -} - -function expectProvidersScenario(defs: { - app?: ComponentTest, - parent?: ComponentTest, - viewChild?: ComponentTest, - contentChild?: ComponentTest, - ngModule?: InjectorType, -}): void { - function testComponentInjection(def: ComponentTest|undefined, instance: T): T { - if (def) { - def.componentAssertion && def.componentAssertion(); - } - return instance; - } - - function testDirectiveInjection(def: ComponentTest|undefined, instance: T): T { - if (def) { - def.directiveAssertion && def.directiveAssertion(); - } - return instance; - } - - class ViewChildComponent { - static ɵfac = () => testComponentInjection(defs.viewChild, new ViewChildComponent()); - static ɵcmp = ɵɵdefineComponent({ - type: ViewChildComponent, - selectors: [['view-child']], - decls: 1, - vars: 0, - template: - function(fs: RenderFlags, ctx: ViewChildComponent) { - if (fs & RenderFlags.Create) { - ɵɵtext(0, 'view-child'); - } - }, - features: defs.viewChild && - [ɵɵProvidersFeature(defs.viewChild.providers || [], defs.viewChild.viewProviders || [])] - }); - } - - class ViewChildDirective { - static ɵfac = () => testDirectiveInjection(defs.viewChild, new ViewChildDirective()); - static ɵdir = ɵɵdefineDirective({ - type: ViewChildDirective, - selectors: [['view-child']], - features: defs.viewChild && [ɵɵProvidersFeature(defs.viewChild.directiveProviders || [])], - }); - } - - class ContentChildComponent { - static ɵfac = - () => { - return testComponentInjection(defs.contentChild, new ContentChildComponent()); - } - - static ɵcmp = ɵɵdefineComponent({ - type: ContentChildComponent, - selectors: [['content-child']], - decls: 1, - vars: 0, - template: - function(fs: RenderFlags, ctx: ParentComponent) { - if (fs & RenderFlags.Create) { - ɵɵtext(0, 'content-child'); - } - }, - features: defs.contentChild && - [ɵɵProvidersFeature( - defs.contentChild.providers || [], defs.contentChild.viewProviders || [])], - }); - } - - class ContentChildDirective { - static ɵfac = - () => { - return testDirectiveInjection(defs.contentChild, new ContentChildDirective()); - } - - static ɵdir = ɵɵdefineDirective({ - type: ContentChildDirective, - selectors: [['content-child']], - features: defs.contentChild && - [ɵɵProvidersFeature(defs.contentChild.directiveProviders || [])], - }); - } - - - class ParentComponent { - static ɵfac = () => testComponentInjection(defs.parent, new ParentComponent()); - static ɵcmp = ɵɵdefineComponent({ - type: ParentComponent, - selectors: [['parent']], - decls: 1, - vars: 0, - template: - function(fs: RenderFlags, ctx: ParentComponent) { - if (fs & RenderFlags.Create) { - ɵɵelement(0, 'view-child'); - } - }, - features: defs.parent && - [ɵɵProvidersFeature(defs.parent.providers || [], defs.parent.viewProviders || [])], - directives: [ViewChildComponent, ViewChildDirective] - }); - } - - class ParentDirective { - static ɵfac = () => testDirectiveInjection(defs.parent, new ParentDirective()); - static ɵdir = ɵɵdefineDirective({ - type: ParentDirective, - selectors: [['parent']], - features: defs.parent && [ɵɵProvidersFeature(defs.parent.directiveProviders || [])], - }); - } - - class ParentDirective2 { - static ɵfac = () => testDirectiveInjection(defs.parent, new ParentDirective2()); - static ɵdir = ɵɵdefineDirective({ - type: ParentDirective2, - selectors: [['parent']], - features: defs.parent && [ɵɵProvidersFeature(defs.parent.directive2Providers || [])], - }); - } - - - class App { - static ɵfac = () => testComponentInjection(defs.app, new App()); - static ɵcmp = ɵɵdefineComponent({ - type: App, - selectors: [['app']], - decls: 2, - vars: 0, - template: - function(fs: RenderFlags, ctx: App) { - if (fs & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - ɵɵelement(1, 'content-child'); - ɵɵelementEnd(); - } - }, - features: defs.app && - [ɵɵProvidersFeature(defs.app.providers || [], defs.app.viewProviders || [])], - directives: - [ - ParentComponent, ParentDirective2, ParentDirective, ContentChildComponent, - ContentChildDirective - ] - }); - } - - - const fixture = new ComponentFixture( - App, {injector: defs.ngModule ? createInjector(defs.ngModule) : undefined}); - expect(fixture.html).toEqual('view-child'); -} diff --git a/packages/router/karma-test-shim.js b/packages/router/karma-test-shim.js deleted file mode 100644 index 63258e2d65cf..000000000000 --- a/packages/router/karma-test-shim.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -/*global jasmine, __karma__, window*/ -Error.stackTraceLimit = 5; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - -__karma__.loaded = function() {}; - -window.isNode = false; -window.isBrowser = true; - -function isJsFile(path) { - return path.slice(-3) == '.js'; -} - -function isSpecFile(path) { - return path.slice(-7) == 'spec.js'; -} - -function isBuiltFile(path) { - var builtPath = '/base/dist/'; - return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); -} - -var allSpecFiles = Object.keys(window.__karma__.files).filter(isSpecFile).filter(isBuiltFile); - -// Load our SystemJS configuration. -System.config({ - baseURL: '/base', -}); - -System.config({ - map: { - '@angular': 'dist/all/@angular', - 'rxjs': 'node_modules/rxjs', - }, - packages: { - '@angular/core/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/core': {main: 'index.js', defaultExtension: 'js'}, - '@angular/compiler/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/compiler': {main: 'index.js', defaultExtension: 'js'}, - '@angular/common/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/common': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser-dynamic/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser-dynamic': {main: 'index.js', defaultExtension: 'js'}, - '@angular/private/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/upgrade/static': {main: 'index.js', defaultExtension: 'js'}, - '@angular/router/upgrade': {main: 'index.js', defaultExtension: 'js'}, - '@angular/router/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/router': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/ajax': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/operators': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/testing': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/websocket': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs': {main: 'index.js', defaultExtension: 'js'}, - } -}); - -Promise - .all([ - System.import('@angular/core/testing'), - System.import('@angular/platform-browser-dynamic/testing') - ]) - .then(function(providers) { - var testing = providers[0]; - var testingBrowser = providers[1]; - - testing.TestBed.initTestEnvironment( - testingBrowser.BrowserDynamicTestingModule, - testingBrowser.platformBrowserDynamicTesting()); - }) - .then(function() { - // Finally, load all spec files. - // This will run the tests directly. - return Promise.all(allSpecFiles.map(function(moduleName) { - return System.import(moduleName); - })); - }) - .then(__karma__.start, function(v) { - console.error(v); - }); diff --git a/packages/router/karma.conf.js b/packages/router/karma.conf.js deleted file mode 100644 index 4d4eccb70d6f..000000000000 --- a/packages/router/karma.conf.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 browserProvidersConf = require('../../browser-providers.conf'); -const {generateSeed} = require('../../tools/jasmine-seed-generator'); - -// Karma configuration -module.exports = function(config) { - config.set({ - - basePath: '../../', - - frameworks: ['jasmine'], - - client: { - jasmine: { - random: true, - seed: generateSeed('router/karma.conf'), - }, - }, - - files: [ - // Polyfills. - 'node_modules/core-js/client/core.js', - 'node_modules/reflect-metadata/Reflect.js', - 'third_party/shims_for_internal_tests.js', - - // System.js for module loading - 'node_modules/systemjs/dist/system-polyfills.js', - 'node_modules/systemjs/dist/system.src.js', - - // Zone.js dependencies - 'dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js', - 'dist/bin/packages/zone.js/npm_package/bundles/zone-testing.umd.js', - - {pattern: 'node_modules/rxjs/**/*', included: false, watched: false}, - - // shim - {pattern: 'packages/router/karma-test-shim.js', included: true, watched: true}, - - // Angular modules - {pattern: 'dist/all/@angular/core/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/core/src/**/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/core/testing/**/*.js', included: false, watched: false}, - - {pattern: 'dist/all/@angular/common/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/common/src/**/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/common/testing/**/*.js', included: false, watched: false}, - - {pattern: 'dist/all/@angular/compiler/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/compiler/src/**/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/compiler/testing/**/*.js', included: false, watched: false}, - - {pattern: 'dist/all/@angular/platform-browser/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/platform-browser/src/**/*.js', included: false, watched: false}, - { - pattern: 'dist/all/@angular/platform-browser/testing/**/*.js', - included: false, - watched: false - }, - - {pattern: 'dist/all/@angular/platform-browser-dynamic/*.js', included: false, watched: false}, - { - pattern: 'dist/all/@angular/platform-browser-dynamic/src/**/*.js', - included: false, - watched: false - }, - { - pattern: 'dist/all/@angular/platform-browser-dynamic/testing/**/*.js', - included: false, - watched: false - }, - - {pattern: 'dist/all/@angular/private/testing/**/*.js', included: false, watched: false}, - - {pattern: 'dist/all/@angular/upgrade/static/*.js', included: false, watched: false}, - {pattern: 'dist/all/@angular/upgrade/static/src/**/*.js', included: false, watched: false}, - - // Router - {pattern: 'dist/all/@angular/router/**/*.js', included: false, watched: true} - ], - - customLaunchers: browserProvidersConf.customLaunchers, - - plugins: [ - 'karma-jasmine', - 'karma-sauce-launcher', - 'karma-chrome-launcher', - 'karma-sourcemap-loader', - ], - - preprocessors: { - '**/*.js': ['sourcemap'], - }, - - reporters: ['dots'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - captureTimeout: 60000, - browserDisconnectTimeout: 60000, - browserDisconnectTolerance: 3, - browserNoActivityTimeout: 60000, - }); -}; diff --git a/packages/router/test/router_preloader.spec.ts b/packages/router/test/router_preloader.spec.ts index 94d5c620b6ec..03dc0c2f6ab7 100644 --- a/packages/router/test/router_preloader.spec.ts +++ b/packages/router/test/router_preloader.spec.ts @@ -7,7 +7,6 @@ */ import {Compiler, Component, Injector, NgModule, NgModuleFactory, NgModuleRef, Type} from '@angular/core'; -import {resolveComponentResources} from '@angular/core/src/metadata/resource_loading'; import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '@angular/router'; import {BehaviorSubject, Observable, of, throwError} from 'rxjs'; diff --git a/packages/tsconfig-legacy-saucelabs.json b/packages/tsconfig-legacy-saucelabs.json index ec661240ab6f..7d73f05d065d 100644 --- a/packages/tsconfig-legacy-saucelabs.json +++ b/packages/tsconfig-legacy-saucelabs.json @@ -1,13 +1,37 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "target": "ES5", - "module": "commonjs", + "outDir": "../dist/legacy-test-out", + "target": "ES2015", + "module": "ES2020", + "importHelpers": true, // The project uses Bazel for TypeScript compilation. Unlike with Bazel, we build all // sources as part of a single TypeScript compilation. This results in `@internal` // declarations not being omitted between the logical as defined per the Bazel targets. // This can cause issues where the `override` keyword is needed for the legacy TS // compilation, but not within Bazel where the overridden member has `@internal`. - "noImplicitOverride": false - } + "noImplicitOverride": false, + // We run the decorator downlevel transform when compiling the sources and tests. + // Given that is the case, we do not need additional `design` metadata being emitted. + // The default TS decorator metadata would also not work with ES2015 JIT. + // https://github.com/angular/angular/issues/30106. + "emitDecoratorMetadata": false + }, + "exclude": [ + // Exclusions from the parent `tsconfig` need to be merged manually here. + "bazel", + "common/locales", + "compiler-cli/integrationtest", + "compiler-cli/test/compliance", + "core/schematics", + "elements/schematics", + "examples/**", + "http/**", + "platform-server/integrationtest", + "router/test/aot_ngsummary_test", + + // Additional exclusion since tests for the language-service never run within the + // Saucelabs job and the package is not compatible with ESM TS compilation anyway. + "language-service/**", + ] } diff --git a/packages/upgrade/static/test/integration/downgrade_module_spec.ts b/packages/upgrade/static/test/integration/downgrade_module_spec.ts index 8b314dfd7d5e..e930b3a357dd 100644 --- a/packages/upgrade/static/test/integration/downgrade_module_spec.ts +++ b/packages/upgrade/static/test/integration/downgrade_module_spec.ts @@ -622,7 +622,9 @@ withEachNg1Version(() => { angular.module_('ng1', [lazyModuleName]) .directive( 'ng2', downgradeComponent({component: Ng2AComponent, propagateDigest})) - .run(($rootScope: angular.IRootScopeService) => $rootScope.value = 0); + .run([ + '$rootScope', ($rootScope: angular.IRootScopeService) => $rootScope.value = 0 + ]); const element = html('
'); const $injector = angular.bootstrap(element, [ng1Module.name]); @@ -811,10 +813,13 @@ withEachNg1Version(() => { const ng1Module = angular.module_('ng1', [lazyModuleName]) .directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest})) - .run(($rootScope: angular.IRootScopeService) => { - $rootScope.attrVal = 'bar'; - $rootScope.propVal = 'bar'; - }); + .run([ + '$rootScope', + ($rootScope: angular.IRootScopeService) => { + $rootScope.attrVal = 'bar'; + $rootScope.propVal = 'bar'; + } + ]); const element = html(''); const $injector = angular.bootstrap(element, [ng1Module.name]); diff --git a/test-main.js b/test-main.js deleted file mode 100644 index 6e8ca29aa7dc..000000000000 --- a/test-main.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 - */ - -// Tun on full stack traces in errors to help debugging -Error.stackTraceLimit = Infinity; - -jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; - -// Cancel Karma's synchronous start, -// we will call `__karma__.start()` later, once all the specs are loaded. -__karma__.loaded = function() {}; - -window.isNode = false; -window.isBrowser = true; - -System.config({ - baseURL: '/base', - defaultJSExtensions: true, - map: { - 'benchpress/*': 'dist/js/dev/es5/benchpress/*.js', - '@angular': 'dist/all/@angular', - 'domino': 'dist/all/@angular/empty.js', - 'url': 'dist/all/@angular/empty.js', - 'xhr2': 'dist/all/@angular/empty.js', - '@angular/platform-server/src/domino_adapter': 'dist/all/@angular/empty.js', - 'angular-in-memory-web-api': 'dist/all/@angular/misc/angular-in-memory-web-api', - 'rxjs': 'node_modules/rxjs', - }, - packages: { - '@angular/core/src/render3': {main: 'index.js', defaultExtension: 'js'}, - '@angular/core/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/core': {main: 'index.js', defaultExtension: 'js'}, - '@angular/animations/browser/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/animations/browser': {main: 'index.js', defaultExtension: 'js'}, - '@angular/animations/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/animations': {main: 'index.js', defaultExtension: 'js'}, - '@angular/compiler/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/compiler': {main: 'index.js', defaultExtension: 'js'}, - '@angular/common/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/common/http/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/common/http': {main: 'index.js', defaultExtension: 'js'}, - '@angular/common': {main: 'index.js', defaultExtension: 'js'}, - '@angular/forms': {main: 'index.js', defaultExtension: 'js'}, - '@angular/misc/angular-in-memory-web-api': {main: 'index.js', defaultExtension: 'js'}, - // remove after all tests imports are fixed - '@angular/facade': {main: 'index.js', defaultExtension: 'js'}, - '@angular/router/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/router': {main: 'index.js', defaultExtension: 'js'}, - '@angular/localize/src/utils': {main: 'index.js', defaultExtension: 'js'}, - '@angular/localize/src/localize': {main: 'index.js', defaultExtension: 'js'}, - '@angular/localize/init': {main: 'index.js', defaultExtension: 'js'}, - '@angular/localize': {main: 'index.js', defaultExtension: 'js'}, - '@angular/upgrade/static/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/upgrade/static': {main: 'index.js', defaultExtension: 'js'}, - '@angular/upgrade': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser/animations/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser/animations': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser-dynamic/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-browser-dynamic': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-server/init': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-server/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/platform-server': {main: 'index.js', defaultExtension: 'js'}, - '@angular/private/testing': {main: 'index.js', defaultExtension: 'js'}, - '@angular/elements': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/ajax': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/operators': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/testing': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs/websocket': {main: 'index.js', defaultExtension: 'js'}, - 'rxjs': {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(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. - .filter(onlySpecFiles) - .map(window.file2moduleName) // Normalize paths to module names. - .map(function(path) { - return System.import(path).then(function(module) { - if (module.hasOwnProperty('main')) { - throw new Error('main() in specs are no longer supported'); - } - }); - })); - }) - - // Kick off karma (Jasmine). - .then( - function() { - __karma__.start(); - }, - function(error) { - console.error(error); - }); - - -function loadCustomElementsPolyfills() { - // The custom elements polyfill will patch properties and methods on `(HTML)Element` and `Node` - // (among others), including `(HTML)Element#innerHTML` and `Node#removeChild()`: - // https://github.com/webcomponents/custom-elements/blob/4f7072c0dbda4beb505d16967acfffd33337b325/src/Patch/Element.js#L28-L73 - // https://github.com/webcomponents/custom-elements/blob/4f7072c0dbda4beb505d16967acfffd33337b325/src/Patch/Node.js#L105-L120 - // The patched `innerHTML` setter and `removeChild()` method will try to traverse the DOM (via - // `nextSibling` and `parentNode` respectively), which leads to infinite loops when testing - // `HtmlSanitizer` with cloberred elements on browsers that do not support the `