diff --git a/extensions/amp-bind/0.1/bind-expression.js b/extensions/amp-bind/0.1/bind-expression.js index d177510c74f94..6c25282c5f1df 100644 --- a/extensions/amp-bind/0.1/bind-expression.js +++ b/extensions/amp-bind/0.1/bind-expression.js @@ -28,15 +28,27 @@ const TAG = 'amp-bind'; */ export let BindExpressionResultDef; +/** + * Default maximum number of nodes in an expression AST. + * Double size of a "typical" expression in examples/bind/performance.amp.html. + * @const @private {number} + */ +const DEFAULT_MAX_AST_SIZE = 50; + /** @const @private {string} */ const BUILT_IN_FUNCTIONS = 'built-in-functions'; /** * Map of object type to function name to whitelisted function. - * @const @private {!Object>} + * @private {!Object>} */ -const FUNCTION_WHITELIST = (function() { +let FUNCTION_WHITELIST; +/** + * @return {!Object>} + * @private + */ +function generateFunctionWhitelist() { /** * Similar to Array.prototype.splice, except it returns a copy of the * passed-in array with the desired modifications. @@ -101,25 +113,21 @@ const FUNCTION_WHITELIST = (function() { Object.keys(whitelist).forEach(type => { out[type] = Object.create(null); - const functions = whitelist[type]; - for (let i = 0; i < functions.length; i++) { - const f = functions[i]; - out[type][f.name] = f; - } + whitelist[type].forEach((fn, i) => { + if (fn) { + out[type][fn.name] = fn; + } else { + // This can happen if a browser doesn't support a built-in function. + throw new Error(`Unsupported function for ${type} at index ${i}.`); + } + }); }); // Custom functions (non-js-built-ins) must be added manually as their names // will be minified at compile time. out[BUILT_IN_FUNCTIONS]['copyAndSplice'] = copyAndSplice; return out; -})(); - -/** - * Default maximum number of nodes in an expression AST. - * Double size of a "typical" expression in examples/bind/performance.amp.html. - * @const @private {number} - */ -const DEFAULT_MAX_AST_SIZE = 50; +} /** * A single Bind expression. @@ -131,6 +139,10 @@ export class BindExpression { * @throws {Error} On malformed expressions. */ constructor(expressionString, opt_maxAstSize) { + if (!FUNCTION_WHITELIST) { + FUNCTION_WHITELIST = generateFunctionWhitelist(); + } + /** @const {string} */ this.expressionString = expressionString; diff --git a/extensions/amp-bind/0.1/bind-impl.js b/extensions/amp-bind/0.1/bind-impl.js index ffdff7b3bfa94..14e44f402db83 100644 --- a/extensions/amp-bind/0.1/bind-impl.js +++ b/extensions/amp-bind/0.1/bind-impl.js @@ -1030,7 +1030,14 @@ export class Bind { */ dispatchEventForTesting_(name) { if (getMode().test) { - this.localWin_.dispatchEvent(new Event(name)); + let event; + if (typeof this.localWin_.Event === 'function') { + event = new Event(name, {bubbles: true, cancelable: true}); + } else { + event = this.localWin_.document.createEvent('Event'); + event.initEvent(name, /* bubbles */ true, /* cancelable */ true); + } + this.localWin_.dispatchEvent(event); } } } diff --git a/extensions/amp-bind/0.1/bind-validator.js b/extensions/amp-bind/0.1/bind-validator.js index ea16c51afec4c..c366b2bb39c6e 100644 --- a/extensions/amp-bind/0.1/bind-validator.js +++ b/extensions/amp-bind/0.1/bind-validator.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {map} from '../../../src/utils/object'; +import {ownProperty} from '../../../src/utils/object'; import {parseSrcset} from '../../../src/srcset'; import {startsWith} from '../../../src/string'; import {user} from '../../../src/log'; @@ -33,21 +33,21 @@ let PropertyRulesDef; * Property rules that apply to any and all tags. * @private {Object} */ -const GLOBAL_PROPERTY_RULES = map({ +const GLOBAL_PROPERTY_RULES = { 'text': null, 'class': { blacklistedValueRegex: '(^|\\W)i-amphtml-', }, -}); +}; /** * Property rules that apply to all AMP elements. - * @private {Object>} + * @private {Object} */ -const AMP_PROPERTY_RULES = map({ +const AMP_PROPERTY_RULES = { 'width': null, 'height': null, -}); +}; /** * Maps tag names to property names to PropertyRulesDef. @@ -61,11 +61,11 @@ const ELEMENT_RULES = createElementRules_(); * Map whose keys comprise all properties that contain URLs. * @private {Object} */ -const URL_PROPERTIES = map({ +const URL_PROPERTIES = { 'src': true, 'srcset': true, 'href': true, -}); +}; /** * BindValidator performs runtime validation of Bind expression results. @@ -113,7 +113,7 @@ export class BindValidator { } // Validate URL(s) if applicable. - if (value && URL_PROPERTIES[property]) { + if (value && ownProperty(URL_PROPERTIES, property)) { let urls; if (property === 'srcset') { let srcset; @@ -181,18 +181,17 @@ export class BindValidator { * @private */ rulesForTagAndProperty_(tag, property) { - const globalPropertyRules = GLOBAL_PROPERTY_RULES[property]; - if (globalPropertyRules !== undefined) { - return globalPropertyRules; + const globalRules = ownProperty(GLOBAL_PROPERTY_RULES, property); + if (globalRules !== undefined) { + return /** @type {PropertyRulesDef} */ (globalRules); } - const tagRules = ELEMENT_RULES[tag]; - // hasOwnProperty() needed since nested objects are not prototype-less. - if (tagRules && tagRules.hasOwnProperty(property)) { + const tagRules = ownProperty(ELEMENT_RULES, tag); + if (tagRules) { return tagRules[property]; } - const ampPropertyRules = AMP_PROPERTY_RULES[property]; + const ampPropertyRules = ownProperty(AMP_PROPERTY_RULES, property); if (startsWith(tag, 'AMP-') && ampPropertyRules !== undefined) { - return ampPropertyRules; + return /** @type {PropertyRulesDef} */ (ampPropertyRules); } return undefined; } @@ -204,7 +203,7 @@ export class BindValidator { */ function createElementRules_() { // Initialize `rules` with tag-specific constraints. - const rules = map({ + const rules = { 'AMP-BRIGHTCOVE': { 'data-account': null, 'data-embed': null, @@ -374,6 +373,6 @@ function createElementRules_() { 'spellcheck': null, 'wrap': null, }, - }); + }; return rules; } diff --git a/gulpfile.js b/gulpfile.js index a912e0c68fdca..064c905ff6a7d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -985,6 +985,7 @@ function buildWebWorker(options) { return compileJs('./src/web-worker/', 'web-worker.js', './dist/', { toName: 'ww.max.js', minifiedName: 'ww.js', + includePolyfills: true, watch: opts.watch, minify: opts.minify || argv.minify, preventRemoveAndMakeDir: opts.preventRemoveAndMakeDir, diff --git a/src/utils/object.js b/src/utils/object.js index 3e28640494b13..ec11178b1504b 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -47,6 +47,22 @@ export function hasOwn(obj, key) { return hasOwn_.call(obj, key); } +/** + * Returns obj[key] iff key is obj's own property (is not inherited). + * Otherwise, returns undefined. + * + * @param {Object} obj + * @param {string} key + * @return {*} + */ +export function ownProperty(obj, key) { + if (hasOwn(obj, key)) { + return obj[key]; + } else { + return undefined; + } +} + /** * @param {!Object} target * @param {!Object} source diff --git a/src/web-worker/web-worker-polyfills.js b/src/web-worker/web-worker-polyfills.js new file mode 100644 index 0000000000000..4cd6d4c59c4d9 --- /dev/null +++ b/src/web-worker/web-worker-polyfills.js @@ -0,0 +1,28 @@ +/** + * Copyright 2017 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Directly imported into web-worker.js entry point so polyfills + * can be used in top-level scope in module dependencies. + */ + +import {install as installArrayIncludes} from '../polyfills/array-includes'; +import {install as installObjectAssign} from '../polyfills/object-assign'; +import {install as installMathSign} from '../polyfills/math-sign'; + +installArrayIncludes(self); +installObjectAssign(self); +installMathSign(self); diff --git a/src/web-worker/web-worker.js b/src/web-worker/web-worker.js index ce073be3047a9..182806b714515 100644 --- a/src/web-worker/web-worker.js +++ b/src/web-worker/web-worker.js @@ -22,9 +22,10 @@ */ import '../../third_party/babel/custom-babel-helpers'; +import './web-worker-polyfills'; import {BindEvaluator} from '../../extensions/amp-bind/0.1/bind-evaluator'; import {FromWorkerMessageDef, ToWorkerMessageDef} from './web-worker-defines'; -import {initLogConstructor} from '../../src/log'; +import {initLogConstructor} from '../log'; import {installWorkerErrorReporting} from '../worker-error-reporting'; initLogConstructor(); diff --git a/test/functional/test-object.js b/test/functional/test-object.js index dde7bf5d068f1..92de6ffd26231 100644 --- a/test/functional/test-object.js +++ b/test/functional/test-object.js @@ -17,12 +17,18 @@ import * as object from '../../src/utils/object'; describe('Object', () => { - it('hasOwn works', () => { + it('hasOwn', () => { expect(object.hasOwn(object.map(), 'a')).to.be.false; expect(object.hasOwn(object.map({'a': 'b'}), 'b')).to.be.false; expect(object.hasOwn(object.map({'a': {}}), 'a')).to.be.true; }); + it('ownProperty', () => { + expect(object.ownProperty({}, '__proto__')).to.be.undefined; + expect(object.ownProperty({}, 'constructor')).to.be.undefined; + expect(object.ownProperty({foo: 'bar'}, 'foo')).to.equal('bar'); + }); + describe('map', () => { it('should make map like objects', () => { expect(object.map().prototype).to.be.undefined; diff --git a/test/integration/test-amp-bind.js b/test/integration/test-amp-bind.js index 25f56a66c7d0b..027a28900003b 100644 --- a/test/integration/test-amp-bind.js +++ b/test/integration/test-amp-bind.js @@ -19,14 +19,15 @@ import {batchedXhrFor, bindForDoc} from '../../src/services'; import {ampdocServiceFor} from '../../src/ampdoc'; import * as sinon from 'sinon'; -// TODO(choumx): Unskip once #9571 is fixed. -describe.skip('amp-bind', function() { +describe.configure().retryOnSaucelabs().run('amp-bind', function() { let fixture; let ampdoc; let sandbox; let numSetStates; let numMutated; + this.timeout(5000); + beforeEach(() => { sandbox = sinon.sandbox.create(); numSetStates = 0; @@ -293,7 +294,8 @@ describe.skip('amp-bind', function() { }); }); - it('should change width and height when their bindings change', () => { + // TODO(choumx): Fix this final flaky test. + it.skip('should change width and height when their bindings change', () => { const button = fixture.doc.getElementById('changeImgDimensButton'); const img = fixture.doc.getElementById('image'); expect(img.getAttribute('height')).to.equal('200');