Skip to content

Commit

Permalink
amp-bind: Fix more integration tests (ampproject#9598)
Browse files Browse the repository at this point in the history
* more bind test fixes

* include object.assign polyfill

* move polyfills to separate file

* replace use of map() from bind-validator with ownProperty()

* fix types and test
  • Loading branch information
William Chou authored and Aaron Turner committed Jun 6, 2017
1 parent 3e24275 commit bb4e676
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 40 deletions.
42 changes: 27 additions & 15 deletions extensions/amp-bind/0.1/bind-expression.js
Expand Up @@ -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<string, !Object<string, Function>>}
* @private {!Object<string, !Object<string, Function>>}
*/
const FUNCTION_WHITELIST = (function() {
let FUNCTION_WHITELIST;

/**
* @return {!Object<string, !Object<string, Function>>}
* @private
*/
function generateFunctionWhitelist() {
/**
* Similar to Array.prototype.splice, except it returns a copy of the
* passed-in array with the desired modifications.
Expand Down Expand Up @@ -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.
Expand All @@ -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;

Expand Down
9 changes: 8 additions & 1 deletion extensions/amp-bind/0.1/bind-impl.js
Expand Up @@ -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);
}
}
}
Expand Down
37 changes: 18 additions & 19 deletions extensions/amp-bind/0.1/bind-validator.js
Expand Up @@ -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';
Expand All @@ -33,21 +33,21 @@ let PropertyRulesDef;
* Property rules that apply to any and all tags.
* @private {Object<string, ?PropertyRulesDef>}
*/
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<string, Object<string, ?PropertyRulesDef>>}
* @private {Object<string, ?PropertyRulesDef>}
*/
const AMP_PROPERTY_RULES = map({
const AMP_PROPERTY_RULES = {
'width': null,
'height': null,
});
};

/**
* Maps tag names to property names to PropertyRulesDef.
Expand All @@ -61,11 +61,11 @@ const ELEMENT_RULES = createElementRules_();
* Map whose keys comprise all properties that contain URLs.
* @private {Object<string, boolean>}
*/
const URL_PROPERTIES = map({
const URL_PROPERTIES = {
'src': true,
'srcset': true,
'href': true,
});
};

/**
* BindValidator performs runtime validation of Bind expression results.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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,
Expand Down Expand Up @@ -374,6 +373,6 @@ function createElementRules_() {
'spellcheck': null,
'wrap': null,
},
});
};
return rules;
}
1 change: 1 addition & 0 deletions gulpfile.js
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions src/utils/object.js
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions 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);
3 changes: 2 additions & 1 deletion src/web-worker/web-worker.js
Expand Up @@ -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();
Expand Down
8 changes: 7 additions & 1 deletion test/functional/test-object.js
Expand Up @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions test/integration/test-amp-bind.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit bb4e676

Please sign in to comment.