diff --git a/Gruntfile.js b/Gruntfile.js index 031612615690..5f602bed8fd7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -358,24 +358,73 @@ module.exports = function(grunt) { }); //alias tasks - grunt.registerTask('test', 'Run unit, docs and e2e tests with Karma', ['eslint', 'package', 'test:unit', 'test:promises-aplus', 'tests:docs', 'test:protractor']); + grunt.registerTask('test', 'Run unit, docs and e2e tests with Karma', [ + 'eslint', + 'package', + 'test:unit', + 'test:promises-aplus', + 'tests:docs', + 'test:protractor' + ]); grunt.registerTask('test:jqlite', 'Run the unit tests with Karma' , ['tests:jqlite']); grunt.registerTask('test:jquery', 'Run the jQuery (latest) unit tests with Karma', ['tests:jquery']); grunt.registerTask('test:jquery-2.2', 'Run the jQuery 2.2 unit tests with Karma', ['tests:jquery-2.2']); grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']); - grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', ['build', 'tests:modules']); + grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', [ + 'build', + 'tests:modules' + ]); grunt.registerTask('test:docs', 'Run the doc-page tests with Karma', ['package', 'tests:docs']); - grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', ['test:jqlite', 'test:jquery', 'test:jquery-2.2', 'test:jquery-2.1', 'test:modules']); - grunt.registerTask('test:protractor', 'Run the end to end tests with Protractor and keep a test server running in the background', ['webdriver', 'connect:testserver', 'protractor:normal']); - grunt.registerTask('test:travis-protractor', 'Run the end to end tests with Protractor for Travis CI builds', ['connect:testserver', 'protractor:travis']); - grunt.registerTask('test:ci-protractor', 'Run the end to end tests with Protractor for Jenkins CI builds', ['webdriver', 'connect:testserver', 'protractor:jenkins']); + grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', [ + 'test:jqlite', + 'test:jquery', + 'test:jquery-2.2', + 'test:jquery-2.1', + 'test:modules' + ]); + grunt.registerTask('test:protractor', 'Run the end to end tests with Protractor and keep a test server running in the background', [ + 'webdriver', + 'connect:testserver', + 'protractor:normal' + ]); + grunt.registerTask('test:travis-protractor', 'Run the end to end tests with Protractor for Travis CI builds', [ + 'connect:testserver', + 'protractor:travis' + ]); + grunt.registerTask('test:ci-protractor', 'Run the end to end tests with Protractor for Jenkins CI builds', [ + 'webdriver', + 'connect:testserver', + 'protractor:jenkins' + ]); grunt.registerTask('test:e2e', 'Alias for test:protractor', ['test:protractor']); - grunt.registerTask('test:promises-aplus',['build:promises-aplus-adapter', 'shell:promises-aplus-tests']); - - grunt.registerTask('minify', ['bower', 'clean', 'build', 'minall']); + grunt.registerTask('test:promises-aplus',[ + 'build:promises-aplus-adapter', + 'shell:promises-aplus-tests' + ]); + grunt.registerTask('minify', [ + 'bower', + 'clean', + 'build', + 'minall' + ]); grunt.registerTask('webserver', ['connect:devserver']); - grunt.registerTask('package', ['bower', 'validate-angular-files', 'clean', 'buildall', 'minall', 'collect-errors', 'write', 'docs', 'copy', 'compress']); - grunt.registerTask('ci-checks', ['ddescribe-iit', 'merge-conflict', 'eslint']); + grunt.registerTask('package', [ + 'bower', + 'validate-angular-files', + 'clean', + 'buildall', + 'minall', + 'collect-errors', + 'write', + 'docs', + 'copy', + 'compress' + ]); + grunt.registerTask('ci-checks', [ + 'ddescribe-iit', + 'merge-conflict', + 'eslint' + ]); grunt.registerTask('default', ['package']); }; diff --git a/karma-shared.conf.js b/karma-shared.conf.js index 1fcf53d4a4d6..65774aa404e8 100644 --- a/karma-shared.conf.js +++ b/karma-shared.conf.js @@ -10,8 +10,16 @@ module.exports = function(config, specificOptions) { browserDisconnectTimeout: 10000, browserDisconnectTolerance: 2, browserNoActivityTimeout: 30000, - - + reporters: ['spec'], + specReporter: { + maxLogLines: 5, // limit number of lines logged per test + suppressErrorSummary: true, // do not print error summary + suppressFailed: false, // do not print information about failed tests + suppressPassed: true, // do not print information about passed tests + suppressSkipped: false, // do not print information about skipped tests + showSpecTiming: false, // print the time elapsed for each spec + failFast: false // test would finish with error when a first fail occurs. + }, // SauceLabs config for local development. sauceLabs: { testName: specificOptions.testName || 'AngularJS', diff --git a/package.json b/package.json index 86df6f1c8259..8c710e67fc7d 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "karma-ng-scenario": "^1.0.0", "karma-sauce-launcher": "^1.1.0", "karma-script-launcher": "^1.0.0", + "karma-spec-reporter": "^0.0.31", "load-grunt-tasks": "^3.5.0", "lodash": "~2.4.1", "log4js": "^0.6.27", diff --git a/scripts/travis/before_build.sh b/scripts/travis/before_build.sh index 0b3356ea358c..76ff7193520f 100755 --- a/scripts/travis/before_build.sh +++ b/scripts/travis/before_build.sh @@ -4,15 +4,26 @@ set -e yarn global add grunt-cli@1.2.0 -mkdir -p $LOGS_DIR +mkdir -p "$LOGS_DIR" -if [ $JOB != "ci-checks" ]; then +if [ "$JOB" != "ci-checks" ]; then echo "start_browser_provider" ./scripts/travis/start_browser_provider.sh fi -if [ $JOB != "ci-checks" ]; then +# ci-checks and unit tests do not run against the packaged code +if [ "$JOB" != "ci-checks" ] && [ "$JOB" != "unit" ]; then grunt package +fi + +# unit runs the docs tests too which need a built version of the code +if [ "$JOB" = "unit" ]; then + grunt build +fi + +# check this after the package, because at this point the browser_provider +# has probably arrived +if [ "$JOB" != "ci-checks" ]; then echo "wait_for_browser_provider" ./scripts/travis/wait_for_browser_provider.sh -fi +fi \ No newline at end of file diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index c193039bc02d..0ecfc25daf5f 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -15,8 +15,8 @@ elif [ "$JOB" == "unit" ]; then fi grunt test:promises-aplus - grunt test:unit --browsers="$BROWSERS" --reporters=dots - grunt tests:docs --browsers="$BROWSERS" --reporters=dots + grunt test:unit --browsers="$BROWSERS" --reporters=spec + grunt tests:docs --browsers="$BROWSERS" --reporters=spec elif [ "$JOB" == "docs-e2e" ]; then grunt test:travis-protractor --specs="docs/app/e2e/**/*.scenario.js" elif [ "$JOB" == "e2e" ]; then diff --git a/src/ng/parse.js b/src/ng/parse.js index 4455129d7ea7..38bb20e7a2a2 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -622,6 +622,9 @@ function isStateless($filter, filterName) { return !fn.$stateful; } +var PURITY_ABSOLUTE = 1; +var PURITY_RELATIVE = 2; + // Detect nodes which could depend on non-shallow state of objects function isPure(node, parentIsPure) { switch (node.type) { @@ -634,18 +637,18 @@ function isPure(node, parentIsPure) { // Unary always convert to primative case AST.UnaryExpression: - return true; + return PURITY_ABSOLUTE; // The binary + operator can invoke a stateful toString(). case AST.BinaryExpression: - return node.operator !== '+'; + return node.operator !== '+' ? PURITY_ABSOLUTE : false; // Functions / filters probably read state from within objects case AST.CallExpression: return false; } - return (undefined === parentIsPure) || parentIsPure; + return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure; } function findConstantAndWatchExpressions(ast, $filter, parentIsPure) { @@ -873,7 +876,7 @@ ASTCompiler.prototype = { forEach(inputs, function(input) { result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); if (input.isPure) { - result.push(input.name, '.isPure=true;'); + result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';'); } }); if (inputs.length) { @@ -1960,10 +1963,16 @@ function $ParseProvider() { fn.$$watchDelegate = watchDelegate; fn.inputs = parsedExpression.inputs; } else if (!interceptorFn.$stateful) { - // If there is an interceptor, but no watchDelegate then treat the interceptor like - // we treat filters - it is assumed to be a pure function unless flagged with $stateful + // Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate fn.$$watchDelegate = inputsWatchDelegate; - fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; + fn.inputs = (parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]).map(function(e) { + // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a + // potentially non-pure interceptor function. + if (e.isPure === PURITY_RELATIVE) { + return function depurifier(s) { return e(s); }; + } + return e; + }); } return fn; diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index 655f3b9cea1c..1c08a1c4133c 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -517,6 +517,22 @@ describe('ngClass', function() { }) ); + // https://github.com/angular/angular.js/issues/15905 + it('should support a mixed literal-array/object variable', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + + $rootScope.classVar = {orange: true}; + $rootScope.$digest(); + expect(element).toHaveClass('orange'); + + $rootScope.classVar.orange = false; + $rootScope.$digest(); + + expect(element).not.toHaveClass('orange'); + }) + ); + + it('should do value stabilization as expected when one-time binding', inject(function($rootScope, $compile) { element = $compile('')($rootScope); diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 3c21649db71e..cfa713d60279 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -3277,6 +3277,25 @@ describe('parser', function() { expect(called).toBe(true); })); + it('should always be invoked if inputs are non-primitive', inject(function($parse) { + var called = false; + function interceptor(v) { + called = true; + return v.sub; + } + + scope.$watch($parse('[o]', interceptor)); + scope.o = {sub: 1}; + + called = false; + scope.$digest(); + expect(called).toBe(true); + + called = false; + scope.$digest(); + expect(called).toBe(true); + })); + it('should not be invoked unless the input.valueOf() changes even if the instance changes', inject(function($parse) { var called = false; function interceptor(v) { @@ -3321,6 +3340,32 @@ describe('parser', function() { scope.$digest(); expect(called).toBe(true); })); + + it('should not affect when a one-time binding becomes stable', inject(function($parse) { + scope.$watch($parse('::x')); + scope.$watch($parse('::x', identity)); + scope.$watch($parse('::x', function() { return 1; })); //interceptor that returns non-undefined + + scope.$digest(); + expect(scope.$$watchersCount).toBe(3); + + scope.x = 1; + scope.$digest(); + expect(scope.$$watchersCount).toBe(0); + })); + + it('should not affect when a one-time literal binding becomes stable', inject(function($parse) { + scope.$watch($parse('::[x]')); + scope.$watch($parse('::[x]', identity)); + scope.$watch($parse('::[x]', function() { return 1; })); //interceptor that returns non-literal + + scope.$digest(); + expect(scope.$$watchersCount).toBe(3); + + scope.x = 1; + scope.$digest(); + expect(scope.$$watchersCount).toBe(0); + })); }); describe('literals', function() { diff --git a/yarn.lock b/yarn.lock index 116e4d1c983d..46d99a35381b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1093,7 +1093,7 @@ colors@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@^1.1.0, colors@~1.1.2: +colors@^1.1.0, colors@^1.1.2, colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -3649,6 +3649,12 @@ karma-script-launcher@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/karma-script-launcher/-/karma-script-launcher-1.0.0.tgz#cd017c4de5ef09e5a9da793276176108dd4b542d" +karma-spec-reporter@^0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/karma-spec-reporter/-/karma-spec-reporter-0.0.31.tgz#4830dc7148a155c7d7a186e632339a0d80fadec3" + dependencies: + colors "^1.1.2" + karma@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269"