Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
@@ -0,0 +1,211 @@
// Copyright 2013 The Closure Library 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 This event monitor wraps the Page Visibility API.
* @see http://www.w3.org/TR/page-visibility/
*/
goog.provide('goog.labs.dom.PageVisibilityEvent');
goog.provide('goog.labs.dom.PageVisibilityMonitor');
goog.provide('goog.labs.dom.PageVisibilityState');
goog.require('goog.dom');
goog.require('goog.dom.vendor');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.memoize');
goog.scope(function() {
var dom = goog.labs.dom;
/**
* The different visibility states.
* @enum {string}
*/
dom.PageVisibilityState = {
HIDDEN: 'hidden',
VISIBLE: 'visible',
PRERENDER: 'prerender',
UNLOADED: 'unloaded'
};
/**
* This event handler allows you to catch page visibility change events.
* @param {!goog.dom.DomHelper=} opt_domHelper
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
dom.PageVisibilityMonitor = function(opt_domHelper) {
dom.PageVisibilityMonitor.base(this, 'constructor');
/**
* @private {!goog.dom.DomHelper}
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
/**
* @private {?string}
*/
this.eventType_ = this.getBrowserEventType_();
// Some browsers do not support visibilityChange and therefore we don't bother
// setting up events.
if (this.eventType_) {
/**
* @private {goog.events.Key}
*/
this.eventKey_ = goog.events.listen(this.domHelper_.getDocument(),
this.eventType_, goog.bind(this.handleChange_, this));
}
};
goog.inherits(dom.PageVisibilityMonitor, goog.events.EventTarget);
/**
* @return {?string} The visibility change event type, or null if not supported.
* Memoized for performance.
* @private
*/
dom.PageVisibilityMonitor.prototype.getBrowserEventType_ =
goog.memoize(function() {
var isSupported = this.isSupported();
var isPrefixed = this.isPrefixed_();
if (isSupported) {
return isPrefixed ? goog.dom.vendor.getPrefixedEventType(
goog.events.EventType.VISIBILITYCHANGE) :
goog.events.EventType.VISIBILITYCHANGE;
} else {
return null;
}
});
/**
* @return {?string} The browser-specific document.hidden property. Memoized
* for performance.
* @private
*/
dom.PageVisibilityMonitor.prototype.getHiddenPropertyName_ = goog.memoize(
function() {
return goog.dom.vendor.getPrefixedPropertyName(
'hidden', this.domHelper_.getDocument());
});
/**
* @return {boolean} Whether the visibility API is prefixed.
* @private
*/
dom.PageVisibilityMonitor.prototype.isPrefixed_ = function() {
return this.getHiddenPropertyName_() != 'hidden';
};
/**
* @return {?string} The browser-specific document.visibilityState property.
* Memoized for performance.
* @private
*/
dom.PageVisibilityMonitor.prototype.getVisibilityStatePropertyName_ =
goog.memoize(function() {
return goog.dom.vendor.getPrefixedPropertyName(
'visibilityState', this.domHelper_.getDocument());
});
/**
* @return {boolean} Whether the visibility API is supported.
*/
dom.PageVisibilityMonitor.prototype.isSupported = function() {
return !!this.getHiddenPropertyName_();
};
/**
* @return {boolean} Whether the page is visible.
*/
dom.PageVisibilityMonitor.prototype.isHidden = function() {
return !!this.domHelper_.getDocument()[this.getHiddenPropertyName_()];
};
/**
* @return {?dom.PageVisibilityState} The page visibility state, or null if
* not supported.
*/
dom.PageVisibilityMonitor.prototype.getVisibilityState = function() {
if (!this.isSupported()) {
return null;
}
return this.domHelper_.getDocument()[this.getVisibilityStatePropertyName_()];
};
/**
* Handles the events on the element.
* @param {goog.events.BrowserEvent} e The underlying browser event.
* @private
*/
dom.PageVisibilityMonitor.prototype.handleChange_ = function(e) {
var state = this.getVisibilityState();
var visibilityEvent = new dom.PageVisibilityEvent(
this.isHidden(), /** @type {dom.PageVisibilityState} */ (state));
this.dispatchEvent(visibilityEvent);
};
/** @override */
dom.PageVisibilityMonitor.prototype.disposeInternal = function() {
goog.events.unlistenByKey(this.eventKey_);
dom.PageVisibilityMonitor.base(this, 'disposeInternal');
};
/**
* A page visibility change event.
* @param {boolean} hidden Whether the page is hidden.
* @param {goog.labs.dom.PageVisibilityState} visibilityState A more detailed
* visibility state.
* @constructor
* @extends {goog.events.Event}
* @final
*/
dom.PageVisibilityEvent = function(hidden, visibilityState) {
dom.PageVisibilityEvent.base(
this, 'constructor', goog.events.EventType.VISIBILITYCHANGE);
/**
* Whether the page is hidden.
* @type {boolean}
*/
this.hidden = hidden;
/**
* A more detailed visibility state.
* @type {dom.PageVisibilityState}
*/
this.visibilityState = visibilityState;
};
goog.inherits(dom.PageVisibilityEvent, goog.events.Event);
}); // goog.scope
@@ -0,0 +1,87 @@
// Copyright 2013 The Closure Library 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.
goog.provide('goog.labs.dom.PageVisibilityMonitorTest');
goog.setTestOnly('goog.labs.dom.PageVisibilityMonitorTest');
goog.require('goog.events');
goog.require('goog.functions');
goog.require('goog.labs.dom.PageVisibilityMonitor');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var stubs = new goog.testing.PropertyReplacer();
var vh;
function tearDown() {
goog.dispose(vh);
vh = null;
stubs.reset();
}
function testConstructor() {
vh = new goog.labs.dom.PageVisibilityMonitor();
}
function testNoVisibilitySupport() {
stubs.set(goog.labs.dom.PageVisibilityMonitor.prototype,
'getBrowserEventType_', goog.functions.NULL);
var listener = goog.testing.recordFunction();
vh = new goog.labs.dom.PageVisibilityMonitor();
goog.events.listen(vh, 'visibilitychange', listener);
var e = new goog.testing.events.Event('visibilitychange');
e.target = window.document;
goog.testing.events.fireBrowserEvent(e);
assertEquals(0, listener.getCallCount());
}
function testListener() {
stubs.set(goog.labs.dom.PageVisibilityMonitor.prototype,
'getBrowserEventType_', goog.functions.constant('visibilitychange'));
var listener = goog.testing.recordFunction();
vh = new goog.labs.dom.PageVisibilityMonitor();
goog.events.listen(vh, 'visibilitychange', listener);
var e = new goog.testing.events.Event('visibilitychange');
e.target = window.document;
goog.testing.events.fireBrowserEvent(e);
assertEquals(1, listener.getCallCount());
}
function testListenerForWebKit() {
stubs.set(goog.labs.dom.PageVisibilityMonitor.prototype,
'getBrowserEventType_',
goog.functions.constant('webkitvisibilitychange'));
var listener = goog.testing.recordFunction();
vh = new goog.labs.dom.PageVisibilityMonitor();
goog.events.listen(vh, 'visibilitychange', listener);
var e = new goog.testing.events.Event('webkitvisibilitychange');
e.target = window.document;
goog.testing.events.fireBrowserEvent(e);
assertEquals(1, listener.getCallCount());
}
@@ -0,0 +1,305 @@
// Copyright 2005 The Closure Library 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 An implementation of {@link goog.events.Listenable} that does
* not need to be disposed.
*/
goog.provide('goog.labs.events.NonDisposableEventTarget');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events.Event');
goog.require('goog.events.Listenable');
goog.require('goog.events.ListenerMap');
goog.require('goog.object');
/**
* An implementation of {@code goog.events.Listenable} with full W3C
* EventTarget-like support (capture/bubble mechanism, stopping event
* propagation, preventing default actions).
*
* You may subclass this class to turn your class into a Listenable.
*
* Unlike {@link goog.events.EventTarget}, this class does not implement
* {@link goog.disposable.IDisposable}. Instances of this class that have had
* It is not necessary to call {@link goog.dispose}
* or {@link #removeAllListeners} in order for an instance of this class
* to be garbage collected.
*
* Unless propagation is stopped, an event dispatched by an
* EventTarget will bubble to the parent returned by
* {@code getParentEventTarget}. To set the parent, call
* {@code setParentEventTarget}. Subclasses that don't support
* changing the parent can override the setter to throw an error.
*
* Example usage:
* <pre>
* var source = new goog.labs.events.NonDisposableEventTarget();
* function handleEvent(e) {
* alert('Type: ' + e.type + '; Target: ' + e.target);
* }
* source.listen('foo', handleEvent);
* source.dispatchEvent('foo'); // will call handleEvent
* </pre>
*
* TODO(chrishenry|johnlenz): Consider a more modern, less viral
* (not based on inheritance) replacement of goog.Disposable, which will allow
* goog.events.EventTarget to not be disposable.
*
* @constructor
* @implements {goog.events.Listenable}
* @final
*/
goog.labs.events.NonDisposableEventTarget = function() {
/**
* Maps of event type to an array of listeners.
* @private {!goog.events.ListenerMap}
*/
this.eventTargetListeners_ = new goog.events.ListenerMap(this);
};
goog.events.Listenable.addImplementation(
goog.labs.events.NonDisposableEventTarget);
/**
* An artificial cap on the number of ancestors you can have. This is mainly
* for loop detection.
* @const {number}
* @private
*/
goog.labs.events.NonDisposableEventTarget.MAX_ANCESTORS_ = 1000;
/**
* Parent event target, used during event bubbling.
* @private {goog.events.Listenable}
*/
goog.labs.events.NonDisposableEventTarget.prototype.parentEventTarget_ = null;
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.getParentEventTarget =
function() {
return this.parentEventTarget_;
};
/**
* Sets the parent of this event target to use for capture/bubble
* mechanism.
* @param {goog.events.Listenable} parent Parent listenable (null if none).
*/
goog.labs.events.NonDisposableEventTarget.prototype.setParentEventTarget =
function(parent) {
this.parentEventTarget_ = parent;
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.dispatchEvent = function(
e) {
this.assertInitialized_();
var ancestorsTree, ancestor = this.getParentEventTarget();
if (ancestor) {
ancestorsTree = [];
var ancestorCount = 1;
for (; ancestor; ancestor = ancestor.getParentEventTarget()) {
ancestorsTree.push(ancestor);
goog.asserts.assert(
(++ancestorCount <
goog.labs.events.NonDisposableEventTarget.MAX_ANCESTORS_),
'infinite loop');
}
}
return goog.labs.events.NonDisposableEventTarget.dispatchEventInternal_(
this, e, ancestorsTree);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.listen = function(
type, listener, opt_useCapture, opt_listenerScope) {
this.assertInitialized_();
return this.eventTargetListeners_.add(
String(type), listener, false /* callOnce */, opt_useCapture,
opt_listenerScope);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.listenOnce = function(
type, listener, opt_useCapture, opt_listenerScope) {
return this.eventTargetListeners_.add(
String(type), listener, true /* callOnce */, opt_useCapture,
opt_listenerScope);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.unlisten = function(
type, listener, opt_useCapture, opt_listenerScope) {
return this.eventTargetListeners_.remove(
String(type), listener, opt_useCapture, opt_listenerScope);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.unlistenByKey = function(
key) {
return this.eventTargetListeners_.removeByKey(key);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.removeAllListeners =
function(opt_type) {
return this.eventTargetListeners_.removeAll(opt_type);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.fireListeners = function(
type, capture, eventObject) {
// TODO(chrishenry): Original code avoids array creation when there
// is no listener, so we do the same. If this optimization turns
// out to be not required, we can replace this with
// getListeners(type, capture) instead, which is simpler.
var listenerArray = this.eventTargetListeners_.listeners[String(type)];
if (!listenerArray) {
return true;
}
listenerArray = goog.array.clone(listenerArray);
var rv = true;
for (var i = 0; i < listenerArray.length; ++i) {
var listener = listenerArray[i];
// We might not have a listener if the listener was removed.
if (listener && !listener.removed && listener.capture == capture) {
var listenerFn = listener.listener;
var listenerHandler = listener.handler || listener.src;
if (listener.callOnce) {
this.unlistenByKey(listener);
}
rv = listenerFn.call(listenerHandler, eventObject) !== false && rv;
}
}
return rv && eventObject.returnValue_ != false;
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.getListeners = function(
type, capture) {
return this.eventTargetListeners_.getListeners(String(type), capture);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.getListener = function(
type, listener, capture, opt_listenerScope) {
return this.eventTargetListeners_.getListener(
String(type), listener, capture, opt_listenerScope);
};
/** @override */
goog.labs.events.NonDisposableEventTarget.prototype.hasListener = function(
opt_type, opt_capture) {
var id = goog.isDef(opt_type) ? String(opt_type) : undefined;
return this.eventTargetListeners_.hasListener(id, opt_capture);
};
/**
* Asserts that the event target instance is initialized properly.
* @private
*/
goog.labs.events.NonDisposableEventTarget.prototype.assertInitialized_ =
function() {
goog.asserts.assert(
this.eventTargetListeners_,
'Event target is not initialized. Did you call the superclass ' +
'(goog.labs.events.NonDisposableEventTarget) constructor?');
};
/**
* Dispatches the given event on the ancestorsTree.
*
* TODO(chrishenry): Look for a way to reuse this logic in
* goog.events, if possible.
*
* @param {!Object} target The target to dispatch on.
* @param {goog.events.Event|Object|string} e The event object.
* @param {Array<goog.events.Listenable>=} opt_ancestorsTree The ancestors
* tree of the target, in reverse order from the closest ancestor
* to the root event target. May be null if the target has no ancestor.
* @return {boolean} If anyone called preventDefault on the event object (or
* if any of the listeners returns false) this will also return false.
* @private
*/
goog.labs.events.NonDisposableEventTarget.dispatchEventInternal_ = function(
target, e, opt_ancestorsTree) {
var type = e.type || /** @type {string} */ (e);
// If accepting a string or object, create a custom event object so that
// preventDefault and stopPropagation work with the event.
if (goog.isString(e)) {
e = new goog.events.Event(e, target);
} else if (!(e instanceof goog.events.Event)) {
var oldEvent = e;
e = new goog.events.Event(type, target);
goog.object.extend(e, oldEvent);
} else {
e.target = e.target || target;
}
var rv = true, currentTarget;
// Executes all capture listeners on the ancestors, if any.
if (opt_ancestorsTree) {
for (var i = opt_ancestorsTree.length - 1; !e.propagationStopped_ && i >= 0;
i--) {
currentTarget = e.currentTarget = opt_ancestorsTree[i];
rv = currentTarget.fireListeners(type, true, e) && rv;
}
}
// Executes capture and bubble listeners on the target.
if (!e.propagationStopped_) {
currentTarget = e.currentTarget = target;
rv = currentTarget.fireListeners(type, true, e) && rv;
if (!e.propagationStopped_) {
rv = currentTarget.fireListeners(type, false, e) && rv;
}
}
// Executes all bubble listeners on the ancestors, if any.
if (opt_ancestorsTree) {
for (i = 0; !e.propagationStopped_ && i < opt_ancestorsTree.length; i++) {
currentTarget = e.currentTarget = opt_ancestorsTree[i];
rv = currentTarget.fireListeners(type, false, e) && rv;
}
}
return rv;
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.events.EventTarget
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.events.NonDisposableEventTargetTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,72 @@
// Copyright 2006 The Closure Library 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.
goog.provide('goog.labs.events.NonDisposableEventTargetTest');
goog.setTestOnly('goog.labs.events.NonDisposableEventTargetTest');
goog.require('goog.events.Listenable');
goog.require('goog.events.eventTargetTester');
goog.require('goog.events.eventTargetTester.KeyType');
goog.require('goog.events.eventTargetTester.UnlistenReturnType');
goog.require('goog.labs.events.NonDisposableEventTarget');
goog.require('goog.testing.jsunit');
function setUp() {
var newListenableFn = function() {
return new goog.labs.events.NonDisposableEventTarget();
};
var listenFn = function(src, type, listener, opt_capt, opt_handler) {
return src.listen(type, listener, opt_capt, opt_handler);
};
var unlistenFn = function(src, type, listener, opt_capt, opt_handler) {
return src.unlisten(type, listener, opt_capt, opt_handler);
};
var unlistenByKeyFn = function(src, key) {
return src.unlistenByKey(key);
};
var listenOnceFn = function(src, type, listener, opt_capt, opt_handler) {
return src.listenOnce(type, listener, opt_capt, opt_handler);
};
var dispatchEventFn = function(src, e) {
return src.dispatchEvent(e);
};
var removeAllFn = function(src, opt_type, opt_capture) {
return src.removeAllListeners(opt_type, opt_capture);
};
var getListenersFn = function(src, type, capture) {
return src.getListeners(type, capture);
};
var getListenerFn = function(src, type, listener, capture, opt_handler) {
return src.getListener(type, listener, capture, opt_handler);
};
var hasListenerFn = function(src, opt_type, opt_capture) {
return src.hasListener(opt_type, opt_capture);
};
goog.events.eventTargetTester.setUp(
newListenableFn, listenFn, unlistenFn, unlistenByKeyFn,
listenOnceFn, dispatchEventFn,
removeAllFn, getListenersFn, getListenerFn, hasListenerFn,
goog.events.eventTargetTester.KeyType.NUMBER,
goog.events.eventTargetTester.UnlistenReturnType.BOOLEAN, false);
}
function tearDown() {
goog.events.eventTargetTester.tearDown();
}
function testRuntimeTypeIsCorrect() {
var target = new goog.labs.events.NonDisposableEventTarget();
assertTrue(goog.events.Listenable.isImplementedBy(target));
}
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.events.EventTarget
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.events.NonDisposableEventTargetGoogEventsTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,78 @@
// Copyright 2006 The Closure Library 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.
goog.provide('goog.labs.events.NonDisposableEventTargetGoogEventsTest');
goog.setTestOnly('goog.labs.events.NonDisposableEventTargetGoogEventsTest');
goog.require('goog.events');
goog.require('goog.events.eventTargetTester');
goog.require('goog.events.eventTargetTester.KeyType');
goog.require('goog.events.eventTargetTester.UnlistenReturnType');
goog.require('goog.labs.events.NonDisposableEventTarget');
goog.require('goog.testing');
goog.require('goog.testing.jsunit');
function setUp() {
var newListenableFn = function() {
return new goog.labs.events.NonDisposableEventTarget();
};
var unlistenByKeyFn = function(src, key) {
return goog.events.unlistenByKey(key);
};
goog.events.eventTargetTester.setUp(
newListenableFn, goog.events.listen, goog.events.unlisten,
unlistenByKeyFn,
goog.events.listenOnce, goog.events.dispatchEvent,
goog.events.removeAll, goog.events.getListeners,
goog.events.getListener, goog.events.hasListener,
goog.events.eventTargetTester.KeyType.NUMBER,
goog.events.eventTargetTester.UnlistenReturnType.BOOLEAN, true);
}
function tearDown() {
goog.events.eventTargetTester.tearDown();
}
function testUnlistenProperCleanup() {
goog.events.listen(eventTargets[0], EventType.A, listeners[0]);
goog.events.unlisten(eventTargets[0], EventType.A, listeners[0]);
goog.events.listen(eventTargets[0], EventType.A, listeners[0]);
eventTargets[0].unlisten(EventType.A, listeners[0]);
}
function testUnlistenByKeyProperCleanup() {
var keyNum = goog.events.listen(eventTargets[0], EventType.A, listeners[0]);
goog.events.unlistenByKey(keyNum);
}
function testListenOnceProperCleanup() {
goog.events.listenOnce(eventTargets[0], EventType.A, listeners[0]);
eventTargets[0].dispatchEvent(EventType.A);
}
function testListenWithObject() {
var obj = {};
obj.handleEvent = goog.testing.recordFunction();
goog.events.listen(eventTargets[0], EventType.A, obj);
eventTargets[0].dispatchEvent(EventType.A);
assertEquals(1, obj.handleEvent.getCallCount());
}
function testListenWithObjectHandleEventReturningFalse() {
var obj = {};
obj.handleEvent = function() { return false; };
goog.events.listen(eventTargets[0], EventType.A, obj);
assertFalse(eventTargets[0].dispatchEvent(EventType.A));
}
@@ -0,0 +1,82 @@
// Copyright 2013 The Closure Library 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 Utilities to abstract mouse and touch events.
*/
goog.provide('goog.labs.events.touch');
goog.provide('goog.labs.events.touch.TouchData');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events.EventType');
goog.require('goog.string');
/**
* Description the geometry and target of an event.
*
* @typedef {{
* clientX: number,
* clientY: number,
* screenX: number,
* screenY: number,
* target: EventTarget
* }}
*/
goog.labs.events.touch.TouchData;
/**
* Takes a mouse or touch event and returns the relevent geometry and target
* data.
* @param {!Event} e A mouse or touch event.
* @return {!goog.labs.events.touch.TouchData}
*/
goog.labs.events.touch.getTouchData = function(e) {
var source = e;
goog.asserts.assert(
goog.string.startsWith(e.type, 'touch') ||
goog.string.startsWith(e.type, 'mouse'),
'Event must be mouse or touch event.');
if (goog.string.startsWith(e.type, 'touch')) {
goog.asserts.assert(
goog.array.contains([
goog.events.EventType.TOUCHCANCEL,
goog.events.EventType.TOUCHEND,
goog.events.EventType.TOUCHMOVE,
goog.events.EventType.TOUCHSTART
], e.type),
'Touch event not of valid type.');
// If the event is end or cancel, take the first changed touch,
// otherwise the first target touch.
source = (e.type == goog.events.EventType.TOUCHEND ||
e.type == goog.events.EventType.TOUCHCANCEL) ?
e.changedTouches[0] : e.targetTouches[0];
}
return {
clientX: source['clientX'],
clientY: source['clientY'],
screenX: source['screenX'],
screenY: source['screenY'],
target: source['target']
};
};
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.events.touch</title>
<script src="../../base.js"></script>
<script>
goog.require('goog.labs.events.touchTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,96 @@
// Copyright 2013 The Closure Library 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 Unit tests for goog.labs.events.touch.
*/
goog.provide('goog.labs.events.touchTest');
goog.require('goog.labs.events.touch');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.events.touchTest');
function testMouseEvent() {
var fakeTarget = {};
var fakeMouseMove = {
'clientX': 1,
'clientY': 2,
'screenX': 3,
'screenY': 4,
'target': fakeTarget,
'type': 'mousemove'
};
var data = goog.labs.events.touch.getTouchData(fakeMouseMove);
assertEquals(1, data.clientX);
assertEquals(2, data.clientY);
assertEquals(3, data.screenX);
assertEquals(4, data.screenY);
assertEquals(fakeTarget, data.target);
}
function testTouchEvent() {
var fakeTarget = {};
var fakeTouch = {
'clientX': 1,
'clientY': 2,
'screenX': 3,
'screenY': 4,
'target': fakeTarget
};
var fakeTouchStart = {
'targetTouches': [fakeTouch],
'target': fakeTarget,
'type': 'touchstart'
};
var data = goog.labs.events.touch.getTouchData(fakeTouchStart);
assertEquals(1, data.clientX);
assertEquals(2, data.clientY);
assertEquals(3, data.screenX);
assertEquals(4, data.screenY);
assertEquals(fakeTarget, data.target);
}
function testTouchChangeEvent() {
var fakeTarget = {};
var fakeTouch = {
'clientX': 1,
'clientY': 2,
'screenX': 3,
'screenY': 4,
'target': fakeTarget
};
var fakeTouchStart = {
'changedTouches': [fakeTouch],
'target': fakeTarget,
'type': 'touchend'
};
var data = goog.labs.events.touch.getTouchData(fakeTouchStart);
assertEquals(1, data.clientX);
assertEquals(2, data.clientY);
assertEquals(3, data.screenX);
assertEquals(4, data.screenY);
assertEquals(fakeTarget, data.target);
}
@@ -0,0 +1,415 @@
// Copyright 2012 The Closure Library 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 Provides a parser that turns a string of well-formed CSV data
* into an array of objects or an array of arrays. All values are returned as
* strings; the user has to convert data into numbers or Dates as required.
* Empty fields (adjacent commas) are returned as empty strings.
*
* This parser uses http://tools.ietf.org/html/rfc4180 as the definition of CSV.
*
* @author nnaze@google.com (Nathan Naze) Ported to Closure
*/
goog.provide('goog.labs.format.csv');
goog.provide('goog.labs.format.csv.ParseError');
goog.provide('goog.labs.format.csv.Token');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug.Error');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.string.newlines');
/**
* @define {boolean} Enable verbose debugging. This is a flag so it can be
* enabled in production if necessary post-compilation. Otherwise, debug
* information will be stripped to minimize final code size.
*/
goog.labs.format.csv.ENABLE_VERBOSE_DEBUGGING = goog.DEBUG;
/**
* Error thrown when parsing fails.
*
* @param {string} text The CSV source text being parsed.
* @param {number} index The index, in the string, of the position of the
* error.
* @param {string=} opt_message A description of the violated parse expectation.
* @constructor
* @extends {goog.debug.Error}
* @final
*/
goog.labs.format.csv.ParseError = function(text, index, opt_message) {
var message;
/**
* @type {?{line: number, column: number}} The line and column of the parse
* error.
*/
this.position = null;
if (goog.labs.format.csv.ENABLE_VERBOSE_DEBUGGING) {
message = opt_message || '';
var info = goog.labs.format.csv.ParseError.findLineInfo_(text, index);
if (info) {
var lineNumber = info.lineIndex + 1;
var columnNumber = index - info.line.startLineIndex + 1;
this.position = {
line: lineNumber,
column: columnNumber
};
message += goog.string.subs(' at line %s column %s',
lineNumber, columnNumber);
message += '\n' + goog.labs.format.csv.ParseError.getLineDebugString_(
info.line.getContent(), columnNumber);
}
}
goog.labs.format.csv.ParseError.base(this, 'constructor', message);
};
goog.inherits(goog.labs.format.csv.ParseError, goog.debug.Error);
/** @inheritDoc */
goog.labs.format.csv.ParseError.prototype.name = 'ParseError';
/**
* Calculate the line and column for an index in a string.
* TODO(nnaze): Consider moving to goog.string.newlines.
* @param {string} str A string.
* @param {number} index An index into the string.
* @return {?{line: !goog.string.newlines.Line, lineIndex: number}} The line
* and index of the line.
* @private
*/
goog.labs.format.csv.ParseError.findLineInfo_ = function(str, index) {
var lines = goog.string.newlines.getLines(str);
var lineIndex = goog.array.findIndex(lines, function(line) {
return line.startLineIndex <= index && line.endLineIndex > index;
});
if (goog.isNumber(lineIndex)) {
var line = lines[lineIndex];
return {
line: line,
lineIndex: lineIndex
};
}
return null;
};
/**
* Get a debug string of a line and a pointing caret beneath it.
* @param {string} str The string.
* @param {number} column The column to point at (1-indexed).
* @return {string} The debug line.
* @private
*/
goog.labs.format.csv.ParseError.getLineDebugString_ = function(str, column) {
var returnString = str + '\n';
returnString += goog.string.repeat(' ', column - 1) + '^';
return returnString;
};
/**
* A token -- a single-character string or a sentinel.
* @typedef {string|!goog.labs.format.csv.Sentinels_}
*/
goog.labs.format.csv.Token;
/**
* Parses a CSV string to create a two-dimensional array.
*
* This function does not process header lines, etc -- such transformations can
* be made on the resulting array.
*
* @param {string} text The entire CSV text to be parsed.
* @param {boolean=} opt_ignoreErrors Whether to ignore parsing errors and
* instead try to recover and keep going.
* @return {!Array<!Array<string>>} The parsed CSV.
*/
goog.labs.format.csv.parse = function(text, opt_ignoreErrors) {
var index = 0; // current char offset being considered
var EOF = goog.labs.format.csv.Sentinels_.EOF;
var EOR = goog.labs.format.csv.Sentinels_.EOR;
var NEWLINE = goog.labs.format.csv.Sentinels_.NEWLINE; // \r?\n
var EMPTY = goog.labs.format.csv.Sentinels_.EMPTY;
var pushBackToken = null; // A single-token pushback.
var sawComma = false; // Special case for terminal comma.
/**
* Push a single token into the push-back variable.
* @param {goog.labs.format.csv.Token} t Single token.
*/
function pushBack(t) {
goog.labs.format.csv.assertToken_(t);
goog.asserts.assert(goog.isNull(pushBackToken));
pushBackToken = t;
}
/**
* @return {goog.labs.format.csv.Token} The next token in the stream.
*/
function nextToken() {
// Give the push back token if present.
if (pushBackToken != null) {
var c = pushBackToken;
pushBackToken = null;
return c;
}
// We're done. EOF.
if (index >= text.length) {
return EOF;
}
// Give the next charater.
var chr = text.charAt(index++);
goog.labs.format.csv.assertToken_(chr);
// Check if this is a newline. If so, give the new line sentinel.
var isNewline = false;
if (chr == '\n') {
isNewline = true;
} else if (chr == '\r') {
// This is a '\r\n' newline. Treat as single token, go
// forward two indicies.
if (index < text.length && text.charAt(index) == '\n') {
index++;
}
isNewline = true;
}
if (isNewline) {
return NEWLINE;
}
return chr;
}
/**
* Read a quoted field from input.
* @return {string} The field, as a string.
*/
function readQuotedField() {
// We've already consumed the first quote by the time we get here.
var start = index;
var end = null;
for (var token = nextToken(); token != EOF; token = nextToken()) {
if (token == '"') {
end = index - 1;
token = nextToken();
// Two double quotes in a row. Keep scanning.
if (token == '"') {
end = null;
continue;
}
// End of field. Break out.
if (token == ',' || token == EOF || token == NEWLINE) {
if (token == NEWLINE) {
pushBack(token);
}
break;
}
if (!opt_ignoreErrors) {
// Ignoring errors here means keep going in current field after
// closing quote. E.g. "ab"c,d splits into abc,d
throw new goog.labs.format.csv.ParseError(
text, index - 1,
'Unexpected character "' + token + '" after quote mark');
} else {
// Fall back to reading the rest of this field as unquoted.
// Note: the rest is guaranteed not start with ", as that case is
// eliminated above.
var prefix = '"' + text.substring(start, index);
var suffix = readField();
if (suffix == EOR) {
pushBack(NEWLINE);
return prefix;
} else {
return prefix + suffix;
}
}
}
}
if (goog.isNull(end)) {
if (!opt_ignoreErrors) {
throw new goog.labs.format.csv.ParseError(
text, text.length - 1,
'Unexpected end of text after open quote');
} else {
end = text.length;
}
}
// Take substring, combine double quotes.
return text.substring(start, end).replace(/""/g, '"');
}
/**
* Read a field from input.
* @return {string|!goog.labs.format.csv.Sentinels_} The field, as a string,
* or a sentinel (if applicable).
*/
function readField() {
var start = index;
var didSeeComma = sawComma;
sawComma = false;
var token = nextToken();
if (token == EMPTY) {
return EOR;
}
if (token == EOF || token == NEWLINE) {
if (didSeeComma) {
pushBack(EMPTY);
return '';
}
return EOR;
}
// This is the beginning of a quoted field.
if (token == '"') {
return readQuotedField();
}
while (true) {
// This is the end of line or file.
if (token == EOF || token == NEWLINE) {
pushBack(token);
break;
}
// This is the end of record.
if (token == ',') {
sawComma = true;
break;
}
if (token == '"' && !opt_ignoreErrors) {
throw new goog.labs.format.csv.ParseError(text, index - 1,
'Unexpected quote mark');
}
token = nextToken();
}
var returnString = (token == EOF) ?
text.substring(start) : // Return to end of file.
text.substring(start, index - 1);
return returnString.replace(/[\r\n]+/g, ''); // Squash any CRLFs.
}
/**
* Read the next record.
* @return {!Array<string>|!goog.labs.format.csv.Sentinels_} A single record
* with multiple fields.
*/
function readRecord() {
if (index >= text.length) {
return EOF;
}
var record = [];
for (var field = readField(); field != EOR; field = readField()) {
record.push(field);
}
return record;
}
// Read all records and return.
var records = [];
for (var record = readRecord(); record != EOF; record = readRecord()) {
records.push(record);
}
return records;
};
/**
* Sentinel tracking objects.
* @enum {!Object}
* @private
*/
goog.labs.format.csv.Sentinels_ = {
/** Empty field */
EMPTY: {},
/** End of file */
EOF: {},
/** End of record */
EOR: {},
/** Newline. \r?\n */
NEWLINE: {}
};
/**
* @param {string} str A string.
* @return {boolean} Whether the string is a single character.
* @private
*/
goog.labs.format.csv.isCharacterString_ = function(str) {
return goog.isString(str) && str.length == 1;
};
/**
* Assert the parameter is a token.
* @param {*} o What should be a token.
* @throws {goog.asserts.AssertionError} If {@ code} is not a token.
* @private
*/
goog.labs.format.csv.assertToken_ = function(o) {
if (goog.isString(o)) {
goog.asserts.assertString(o);
goog.asserts.assert(goog.labs.format.csv.isCharacterString_(o),
'Should be a string of length 1 or a sentinel.');
} else {
goog.asserts.assert(
goog.object.containsValue(goog.labs.format.csv.Sentinels_, o),
'Should be a string of length 1 or a sentinel.');
}
};
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>csv.js unit tests</title>
<script type="text/javascript"
src="../../base.js">
</script>
<script>
goog.require('goog.labs.format.csvTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,201 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.format.csvTest');
goog.require('goog.labs.format.csv');
goog.require('goog.labs.format.csv.ParseError');
goog.require('goog.object');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.format.csvTest');
function testGoldenPath() {
assertObjectEquals(
[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
goog.labs.format.csv.parse('a,b,c\nd,e,f\ng,h,i\n'));
assertObjectEquals(
[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
goog.labs.format.csv.parse('a,b,c\r\nd,e,f\r\ng,h,i\r\n'));
}
function testNoCrlfAtEnd() {
assertObjectEquals(
[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
goog.labs.format.csv.parse('a,b,c\nd,e,f\ng,h,i'));
}
function testQuotes() {
assertObjectEquals(
[['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']],
goog.labs.format.csv.parse('a,"b",c\n"d","e","f"\ng,h,"i"'));
assertObjectEquals(
[['a', 'b, as in boy', 'c'], ['d', 'e', 'f']],
goog.labs.format.csv.parse('a,"b, as in boy",c\n"d","e","f"\n'));
}
function testEmbeddedCrlfs() {
assertObjectEquals(
[['a', 'b\nball', 'c'], ['d\nd', 'e', 'f'], ['g', 'h', 'i']],
goog.labs.format.csv.parse('a,"b\nball",c\n"d\nd","e","f"\ng,h,"i"\n'));
}
function testEmbeddedQuotes() {
assertObjectEquals(
[['a', '"b"', 'Jonathan "Smokey" Feinberg'], ['d', 'e', 'f']],
goog.labs.format.csv.parse(
'a,"""b""","Jonathan ""Smokey"" Feinberg"\nd,e,f\r\n'));
}
function testUnclosedQuote() {
var e = assertThrows(function() {
goog.labs.format.csv.parse('a,"b,c\nd,e,f');
});
assertTrue(e instanceof goog.labs.format.csv.ParseError);
assertEquals(2, e.position.line);
assertEquals(5, e.position.column);
assertEquals(
'Unexpected end of text after open quote at line 2 column 5\n' +
'd,e,f\n' +
' ^',
e.message);
}
function testQuotesInUnquotedField() {
var e = assertThrows(function() {
goog.labs.format.csv.parse('a,b "and" b,c\nd,e,f');
});
assertTrue(e instanceof goog.labs.format.csv.ParseError);
assertEquals(1, e.position.line);
assertEquals(5, e.position.column);
assertEquals(
'Unexpected quote mark at line 1 column 5\n' +
'a,b "and" b,c\n' +
' ^',
e.message);
}
function testGarbageOutsideQuotes() {
var e = assertThrows(function() {
goog.labs.format.csv.parse('a,"b",c\nd,"e"oops,f');
});
assertTrue(e instanceof goog.labs.format.csv.ParseError);
assertEquals(2, e.position.line);
assertEquals(6, e.position.column);
assertEquals(
'Unexpected character "o" after quote mark at line 2 column 6\n' +
'd,"e"oops,f\n' +
' ^',
e.message);
}
function testEmptyRecords() {
assertObjectEquals(
[['a', '', 'c'], ['d', 'e', ''], ['', '', '']],
goog.labs.format.csv.parse('a,,c\r\nd,e,\n,,'));
}
function testIgnoringErrors() {
// The results of these tests are not defined by the RFC. They
// generally strive to be "reasonable" while keeping the code simple.
// Quotes inside field
assertObjectEquals(
[['Hello "World"!', 'b'], ['c', 'd']], goog.labs.format.csv.parse(
'Hello "World"!,b\nc,d', true));
// Missing closing quote
assertObjectEquals(
[['Hello', 'World!']], goog.labs.format.csv.parse(
'Hello,"World!', true));
// Broken use of quotes in quoted field
assertObjectEquals(
[['a', '"Hello"World!"']], goog.labs.format.csv.parse(
'a,"Hello"World!"', true));
// All of the above. A real mess.
assertObjectEquals(
[['This" is', '"very\n\tvery"broken"', ' indeed!']],
goog.labs.format.csv.parse(
'This" is,"very\n\tvery"broken"," indeed!', true));
}
function testIgnoringErrorsTrailingTabs() {
assertObjectEquals(
[['"a\tb"\t'], ['c,d']], goog.labs.format.csv.parse(
'"a\tb"\t\n"c,d"', true));
}
function testFindLineInfo() {
var testString = 'abc\ndef\rghi';
var info = goog.labs.format.csv.ParseError.findLineInfo_(testString, 4);
assertEquals(4, info.line.startLineIndex);
assertEquals(7, info.line.endContentIndex);
assertEquals(8, info.line.endLineIndex);
assertEquals(1, info.lineIndex);
}
function testGetLineDebugString() {
var str = 'abcdefghijklmnop';
var index = str.indexOf('j');
var column = index + 1;
assertEquals(
goog.labs.format.csv.ParseError.getLineDebugString_(str, column),
'abcdefghijklmnop\n' +
' ^');
}
function testIsCharacterString() {
assertTrue(goog.labs.format.csv.isCharacterString_('a'));
assertTrue(goog.labs.format.csv.isCharacterString_('\n'));
assertTrue(goog.labs.format.csv.isCharacterString_(' '));
assertFalse(goog.labs.format.csv.isCharacterString_(null));
assertFalse(goog.labs.format.csv.isCharacterString_(' '));
assertFalse(goog.labs.format.csv.isCharacterString_(''));
assertFalse(goog.labs.format.csv.isCharacterString_('aa'));
}
function testAssertToken() {
goog.labs.format.csv.assertToken_('a');
goog.object.forEach(goog.labs.format.csv.SENTINELS_,
function(value) {
goog.labs.format.csv.assertToken_(value);
});
assertThrows(function() {
goog.labs.format.csv.assertToken_('aa');
});
assertThrows(function() {
goog.labs.format.csv.assertToken_('');
});
assertThrows(function() {
goog.labs.format.csv.assertToken_({});
});
}
@@ -0,0 +1,74 @@
// Copyright 2014 The Closure Library 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.
goog.provide('goog.labs.html.AttributeRewriter');
goog.provide('goog.labs.html.AttributeValue');
goog.provide('goog.labs.html.attributeRewriterPresubmitWorkaround');
/**
* The type of an attribute value.
* <p>
* Many HTML attributes contain structured data like URLs, CSS, or even entire
* HTML documents, so the type is a union of several variants.
*
* @typedef {(string |
* goog.html.SafeHtml | goog.html.SafeStyle | goog.html.SafeUrl)}
*/
goog.labs.html.AttributeValue;
/**
* A function that takes an attribute value, and returns a safe value.
* <p>
* Since rewriters can be chained, a rewriter must be able to accept the output
* of another rewriter, instead of just a string though a rewriter that coerces
* its input to a string before checking its safety will fail safe.
* <p>
* The meaning of the result is:
* <table>
* <tr><td>{@code null}</td>
* <td>Unsafe. The attribute should not be output.</tr>
* <tr><td>a string</td>
* <td>The plain text (not HTML-entity encoded) of a safe attribute
* value.</td>
* <tr><td>a {@link goog.html.SafeHtml}</td>
* <td>A fragment that is safe to be included as embedded HTML as in
* {@code <iframe srchtml="...">}</td></tr>
* <tr><td>a {@link goog.html.SafeUrl}</td>
* <td>A URL that does not need to be further checked against the URL
* white-list.</td></tr>
* <tr><td>a {@link goog.html.SafeStyle}</td>
* <td>A safe value for a <code>style="..."</code> attribute.</td></tr>
* </table>
* <p>
* Implementations are responsible for making sure that "safe" complies with
* the contract established by the safe string types in {@link goog.html}.
* </p>
*
* @typedef {function(goog.labs.html.AttributeValue) :
* goog.labs.html.AttributeValue}
*/
goog.labs.html.AttributeRewriter;
/**
* g4 presubmit complains about requires of this file because its clients
* don't use any symbols from it outside JSCompiler comment annotations.
* genjsdeps.sh doesn't generate the right dependency graph unless this
* file is required.
* Clients can mention this noop.
*/
goog.labs.html.attributeRewriterPresubmitWorkaround = function() {};
@@ -0,0 +1,392 @@
// Copyright 2014 The Closure Library 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
* An HTML sanitizer that takes untrusted HTML snippets and produces
* safe HTML by filtering/rewriting tags and attributes that contain
* high-privilege instructions.
*/
goog.provide('goog.labs.html.Sanitizer');
goog.require('goog.asserts');
goog.require('goog.html.SafeUrl');
goog.require('goog.labs.html.attributeRewriterPresubmitWorkaround');
goog.require('goog.labs.html.scrubber');
goog.require('goog.object');
goog.require('goog.string');
/**
* A sanitizer that converts untrusted, messy HTML into more regular HTML
* that cannot abuse high-authority constructs like the ability to execute
* arbitrary JavaScript.
* @constructor
*/
goog.labs.html.Sanitizer = function() {
/**
* Maps the lower-case names of allowed elements to attribute white-lists.
* An attribute white-list maps lower-case attribute names to functions
* from values to values or undefined to disallow.
*
* The special element name {@code "*"} contains a white-list of attributes
* allowed on any tag, which is useful for attributes like {@code title} and
* {@code id} which are widely available with element-agnostic meanings.
* It should not be used for attributes like {@code type} whose meaning
* differs based on the element on which it appears:
* e.g. {@code <input type=text>} vs {@code <style type=text/css>}.
*
* @type {!Object<string, !Object<string, goog.labs.html.AttributeRewriter>>}
* @private
*/
this.whitelist_ = goog.labs.html.Sanitizer.createBlankObject_();
this.whitelist_['*'] = goog.labs.html.Sanitizer.createBlankObject_();
// To use the sanitizer, we build inputs for the scrubber.
// These inputs are invalidated by changes to the policy, so we (re)build them
// lazily.
/**
* Maps element names to {@code true} so the scrubber does not have to do
* own property checks for every tag filtered.
*
* Built lazily and invalidated when the white-list is modified.
*
* @type {Object<string, boolean>}
* @private
*/
this.allowedElementSet_ = null;
};
// TODO(user): Should the return type be goog.html.SafeHtml?
// If we receive a safe HTML string as input, should we simply rebalance
// tags?
/**
* Yields a string of safe HTML that contains all and only the safe
* text-nodes and elements in the input.
*
* <p>
* For the purposes of this function, "safe" is defined thus:
* <ul>
* <li>Contains only elements explicitly allowed via {@code this.allow*}.
* <li>Contains only attributes explicitly allowed via {@code this.allow*}
* and having had all relevant transformations applied.
* <li>Contains an end tag for all and only non-void open tags.
* <li>Tags nest per XHTML rules.
* <li>Tags do not nest beyond a finite but fairly large level.
* </ul>
*
* @param {!string} unsafeHtml A string of HTML which need not originate with
* a trusted source.
* @return {!string} A string of HTML that contains only tags and attributes
* explicitly allowed by this sanitizer, and with end tags for all and only
* non-void elements.
*/
goog.labs.html.Sanitizer.prototype.sanitize = function(unsafeHtml) {
var unsafeHtmlString = '' + unsafeHtml;
/**
* @type {!Object<string, !Object<string, goog.labs.html.AttributeRewriter>>}
*/
var whitelist = this.whitelist_;
if (!this.allowedElementSet_) {
this.allowedElementSet_ = goog.object.createSet(
// This can lead to '*' in the allowed element set, but the scrubber
// will not parse "<*" as a tag beginning.
goog.object.getKeys(whitelist));
}
return goog.labs.html.scrubber.scrub(
this.allowedElementSet_, whitelist, unsafeHtmlString);
};
/**
* Adds the element names to the white-list of elements that are allowed
* in the safe HTML output.
* <p>
* Allowing elements does not, by itself, allow any attributes on
* those elements.
*
* @param {...!string} var_args element names that should be allowed in the
* safe HTML output.
* @return {!goog.labs.html.Sanitizer} {@code this}.
*/
goog.labs.html.Sanitizer.prototype.allowElements = function(var_args) {
this.allowedElementSet_ = null; // Invalidate.
var whitelist = this.whitelist_;
for (var i = 0; i < arguments.length; ++i) {
var elementName = arguments[i].toLowerCase();
goog.asserts.assert(
goog.labs.html.Sanitizer.isValidHtmlName_(elementName), elementName);
if (!Object.prototype.hasOwnProperty.call(whitelist, elementName)) {
whitelist[elementName] = goog.labs.html.Sanitizer.createBlankObject_();
}
}
return this;
};
/**
* Allows in the sanitized output
* <tt>&lt;<i>element</i> <i>attr</i>="..."&gt;</tt>
* when <i>element</i> is in {@code elementNames} and
* <i>attrNames</i> is in {@code attrNames}.
*
* If specified, {@code opt_valueXform} is a function that takes the
* HTML-entity-decoded attribute value, and can choose to disallow the
* attribute by returning {@code null} or substitute a new value
* by returning a string with the new value.
*
* @param {!Array<string>|string} elementNames names (or name) on which the
* attributes are allowed.
*
* Element names should be allowed via {@code allowElements(...)} prior
* to white-listing attributes.
*
* The special element name {@code "*"} has the same meaning as in CSS
* selectors: it can be used to white-list attributes like {@code title}
* and {@code id} which are widely available with element-agnostic
* meanings.
*
* It should not be used for attributes like {@code type} whose meaning
* differs based on the element on which it appears:
* e.g. {@code <input type=text>} vs {@code <style type=text/css>}.
*
* @param {!Array<string>|string} attrNames names (or name) of the attribute
* that should be allowed.
*
* @param {goog.labs.html.AttributeRewriter=} opt_rewriteValue A function
* that receives the HTML-entity-decoded attribute value and can return
* {@code null} to disallow the attribute entirely or the value for the
* attribute as a string.
* <p>
* The default is the identity function ({@code function(x){return x}}),
* and the value rewriter is composed with an attribute specific handler:
* <table>
* <tr>
* <th>href, src</th>
* <td>Requires that the value be an absolute URL with a protocol in
* (http, https, mailto) or a protocol relative URL.
* </tr>
* </table>
*
* @return {!goog.labs.html.Sanitizer} {@code this}.
*/
goog.labs.html.Sanitizer.prototype.allowAttributes =
function(elementNames, attrNames, opt_rewriteValue) {
if (!goog.isArray(elementNames)) {
elementNames = [elementNames];
}
if (!goog.isArray(attrNames)) {
attrNames = [attrNames];
}
goog.asserts.assert(
!opt_rewriteValue || 'function' === typeof opt_rewriteValue,
'opt_rewriteValue should be a function');
var whitelist = this.whitelist_;
for (var ei = 0; ei < elementNames.length; ++ei) {
var elementName = elementNames[ei].toLowerCase();
goog.asserts.assert(
goog.labs.html.Sanitizer.isValidHtmlName_(elementName) ||
'*' === elementName,
elementName);
// If the element has not been white-listed then panic.
// TODO(user): allow allow{Elements,Attributes} to be called in any
// order if someone needs it.
if (!Object.prototype.hasOwnProperty.call(whitelist, elementName)) {
throw new Error(elementName);
}
var attrWhitelist = whitelist[elementName];
for (var ai = 0, an = attrNames.length; ai < an; ++ai) {
var attrName = attrNames[ai].toLowerCase();
goog.asserts.assert(
goog.labs.html.Sanitizer.isValidHtmlName_(attrName), attrName);
// If the value has already been allowed, then chain the rewriters
// so that both white-listers concerns are met.
// We do not use the default rewriter here since it should have
// been introduced by the call that created the initial white-list
// entry.
attrWhitelist[attrName] = goog.labs.html.Sanitizer.chain_(
opt_rewriteValue || goog.labs.html.Sanitizer.valueIdentity_,
Object.prototype.hasOwnProperty.call(attrWhitelist, attrName) ?
attrWhitelist[attrName] :
goog.labs.html.Sanitizer.defaultRewriterForAttr_(attrName));
}
}
return this;
};
/**
* A new object that is as blank as possible.
*
* Using {@code Object.create} to create an object with
* no prototype speeds up whitelist access since there's fewer prototypes
* to fall-back to for a common case where an element is not in the
* white-list, and reduces the chance of confusing a member of
* {@code Object.prototype} with a whitelist entry.
*
* @return {!Object<string, ?>} a reference to a newly allocated object that
* does not alias any reference that existed prior.
* @private
*/
goog.labs.html.Sanitizer.createBlankObject_ = function() {
return (Object.create || Object)(null);
};
/**
* HTML element and attribute names may be almost arbitrary strings, but the
* sanitizer is more restrictive as to what can be white-listed.
*
* Since HTML is case-insensitive, only lower-case identifiers composed of
* ASCII letters, digits, and select punctuation are allowed.
*
* @param {string} name
* @return {boolean} true iff name is a valid white-list key.
* @private
*/
goog.labs.html.Sanitizer.isValidHtmlName_ = function(name) {
return 'string' === typeof name && // Names must be strings.
// Names must be lower-case and ASCII identifier chars only.
/^[a-z][a-z0-9\-:]*$/.test(name);
};
/**
* @param {goog.labs.html.AttributeValue} x
* @return {goog.labs.html.AttributeValue}
* @private
*/
goog.labs.html.Sanitizer.valueIdentity_ = function(x) {
return x;
};
/**
* @param {goog.labs.html.AttributeValue} x
* @return {null}
* @private
*/
goog.labs.html.Sanitizer.disallow_ = function(x) {
return null;
};
/**
* Chains attribute rewriters.
*
* @param {goog.labs.html.AttributeRewriter} f
* @param {goog.labs.html.AttributeRewriter} g
* @return {goog.labs.html.AttributeRewriter}
* a function that return g(f(x)) or null if f(x) is null.
* @private
*/
goog.labs.html.Sanitizer.chain_ = function(f, g) {
// Sometimes white-listing code ends up allowing things multiple times.
if (f === goog.labs.html.Sanitizer.valueIdentity_) {
return g;
}
if (g === goog.labs.html.Sanitizer.valueIdentity_) {
return f;
}
// If someone tries to white-list a really problematic value, we reject
// it by returning disallow_. Disallow it quickly.
if (f === goog.labs.html.Sanitizer.disallow_) {
return f;
}
if (g === goog.labs.html.Sanitizer.disallow_) {
return g;
}
return (
/**
* @param {goog.labs.html.AttributeValue} x
* @return {goog.labs.html.AttributeValue}
*/
function(x) {
var y = f(x);
return y != null ? g(y) : null;
});
};
/**
* Given an attribute name, returns a value rewriter that enforces some
* minimal safety properties.
*
* <p>
* For url atributes, it checks that any protocol is on a safe set that
* doesn't allow script execution.
* <p>
* It also blanket disallows CSS and event handler attributes.
*
* @param {string} attrName lower-cased attribute name.
* @return {goog.labs.html.AttributeRewriter}
* @private
*/
goog.labs.html.Sanitizer.defaultRewriterForAttr_ = function(attrName) {
if ('href' === attrName || 'src' === attrName) {
return goog.labs.html.Sanitizer.checkUrl_;
} else if ('style' === attrName || 'on' === attrName.substr(0, 2)) {
// TODO(user): delegate to a CSS sanitizer if one is available.
return goog.labs.html.Sanitizer.disallow_;
}
return goog.labs.html.Sanitizer.valueIdentity_;
};
/**
* Applied automatically to URL attributes to check that they are safe as per
* {@link SafeUrl}.
*
* @param {goog.labs.html.AttributeValue} attrValue a decoded attribute value.
* @return {goog.html.SafeUrl | null} a URL that is equivalent to the
* input or {@code null} if the input is not a safe URL.
* @private
*/
goog.labs.html.Sanitizer.checkUrl_ = function(attrValue) {
if (attrValue == null) {
return null;
}
/** @type {!goog.html.SafeUrl} */
var safeUrl;
if (attrValue instanceof goog.html.SafeUrl) {
safeUrl = /** @type {!goog.html.SafeUrl} */ (attrValue);
} else {
if (typeof attrValue === 'string') {
// Whitespace at the ends of URL-valued attributes in HTML is ignored.
attrValue = goog.string.trim(/** @type {string} */ (attrValue));
}
safeUrl = goog.html.SafeUrl.sanitize(
/** @type {!goog.string.TypedString | string} */ (attrValue));
}
if (goog.html.SafeUrl.unwrap(safeUrl) == goog.html.SafeUrl.INNOCUOUS_STRING) {
return null;
} else {
return safeUrl;
}
};
goog.labs.html.attributeRewriterPresubmitWorkaround();
@@ -0,0 +1,270 @@
// Copyright 2013 The Closure Library 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.
goog.provide('goog.labs.html.SanitizerTest');
goog.require('goog.html.SafeUrl');
goog.require('goog.labs.html.Sanitizer');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.html.SanitizerTest');
var JENNYS_PHONE_NUMBER = goog.html.SafeUrl.fromConstant(
goog.string.Const.from('tel:867-5309'));
var sanitizer = new goog.labs.html.Sanitizer()
.allowElements(
'a', 'b', 'i', 'p', 'font', 'hr', 'br', 'span',
'ol', 'ul', 'li',
'table', 'tr', 'td', 'th', 'tbody',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'img',
'html', 'head', 'body', 'title'
)
// allow unfiltered title attributes, and
.allowAttributes('*', 'title')
// specific dir values.
.allowAttributes(
'*', 'dir',
function(dir) { return dir === 'ltr' || dir === 'rtl' ? dir : null; })
.allowAttributes(
// Specifically on <a> elements,
'a',
// allow an href but verify and rewrite, and
'href',
function(href) {
if (href === 'tel:867-5309') {
return JENNYS_PHONE_NUMBER;
}
// Missing anchor is an intentional error.
return /https?:\/\/google\.[a-z]{2,3}\/search\?/.test(String(href)) ?
href : null;
})
.allowAttributes(
'a',
// mask the generic title handler for no good reason.
'title',
function(title) { return '<' + title + '>'; });
function run(input, golden, desc) {
var actual = sanitizer.sanitize(input);
assertEquals(desc, golden, actual);
}
function testEmptyString() {
run('', '', 'Empty string');
}
function testHelloWorld() {
run('Hello, <b>World</b>!', 'Hello, <b>World</b>!',
'Hello World');
}
function testNoEndTag() {
run('<i>Hello, <b>World!',
'<i>Hello, <b>World!</b></i>',
'Hello World no end tag');
}
function testUnclosedTags() {
run('<html><head><title>Hello, <<World>>!</TITLE>' +
'</head><body><p>Hello,<Br><<World>>!',
'<html><head><title>Hello, <<World>>!</title>' +
'</head><body><p>Hello,<br>&lt;&gt;!</p></body></html>',
'RCDATA content, different case, unclosed tags');
}
function testListInList() {
run('<ul><li>foo</li><ul><li>bar</li></ul></ul>',
'<ul><li>foo</li><li><ul><li>bar</li></ul></li></ul>',
'list in list directly');
}
function testHeaders() {
run('<h1>header</h1>body' +
'<H2>sub-header</h3>sub-body' +
'<h3>sub-sub-</hr>header<hr></hr>sub-sub-body</H4></h2>',
'<h1>header</h1>body' +
'<h2>sub-header</h2>sub-body' +
'<h3>sub-sub-header</h3><hr>sub-sub-body',
'headers');
}
function testListNesting() {
run('<ul><li><ul><li>foo</li></li><ul><li>bar',
'<ul><li><ul><li>foo</li><li><ul><li>bar</li></ul></li></ul></li></ul>',
'list nesting');
}
function testTableNesting() {
run('<table><tbody><tr><td>foo</td><table><tbody><tr><th>bar</table></table>',
'<table><tbody><tr><td>foo</td><td>' +
'<table><tbody><tr><th>bar</th></tr></tbody></table>' +
'</td></tr></tbody></table>',
'table nesting');
}
function testNestingLimit() {
run(goog.string.repeat('<span>', 264) + goog.string.repeat('</span>', 264),
goog.string.repeat('<span>', 256) + goog.string.repeat('</span>', 256),
'264 open spans');
}
function testTableScopes() {
run('<html><head></head><body><p>Hi</p><p>How are you</p>\n' +
'<p><table><tbody><tr>' +
'<td><b><font><font><p>Cell</b></font></font></p>\n</td>' +
'<td><b><font><font><p>Cell</b></font></font></p>\n</td>' +
'</tr></tbody></table></p>\n' +
'<p>x</p></body></html>',
'<html><head></head><body><p>Hi</p><p>How are you</p>\n' +
'<p><table><tbody><tr>' +
'<td><b><font><font></font></font></b><p>Cell</p>\n</td>' +
// The close </p> tag does not close the whole table. +
'<td><b><font><font></font></font></b><p>Cell</p>\n</td>' +
'</tr></tbody></table></p>\n' +
'<p>x</p></body></html>',
'Table Scopes');
}
function testConcatSafe() {
run('<<applet>script<applet>>alert(1337)<<!-- -->/script<?...?>>',
'&lt;script&gt;alert(1337)&lt;/script&gt;',
'Concat safe');
}
function testPrototypeMembersDoNotInfectTables() {
// Constructor is all lower-case so will survive tag name
// normalization.
run('<constructor>Foo</constructor>', 'Foo',
'Object.prototype members');
}
function testGenericAttributesAllowed() {
run('<span title=howdy></span>', '<span title="howdy"></span>',
'generic attrs allowed');
}
function testValueWhitelisting() {
run('<span dir=\'ltr\'>LTR</span><span dir=\'evil\'>Evil</span>',
'<span dir="ltr">LTR</span><span>Evil</span>',
'value whitelisted');
}
function testAttributeNormalization() {
run('<a href="http://google.com/search?q=tests suxor&hl=en">Click</a>',
'<a href="http://google.com/search?q=tests%20suxor&amp;hl=en">Click</a>',
'URL normalized');
}
function testNaiveAttributeRewriterCaught() {
run('<a href="javascript:http://google.com/&#10;alert(1337)">sneaky</a>',
'<a>sneaky</a>',
'Safety net saves naive attribute rewriters');
}
function testSafeUrlFromAttributeRewriter() {
run('<a href="tel:867-5309">Jenny</a>', '<a href="tel:867-5309">Jenny</a>',
'Attribute rewriter escapes safety checks via SafeURL');
}
function testTagSpecificityOfAttributeFiltering() {
run('<img href="http://google.com/search?q=tests+suxor">',
'<img>',
'href blocked on img');
}
function testTagSpecificAttributeFiltering() {
run('<a href="http://google.evil.com/search?q=tests suxor">Unclicky</a>',
'<a>Unclicky</a>',
'bad href value blocked');
}
function testNonWhitelistFunctionsNotCalled() {
var called = false;
Object.prototype.dontcallme = function() {
called = true;
return 'dontcallme was called despite being on the prototype';
};
try {
run('<span dontcallme="I\'ll call you">Lorem Ipsum',
'<span>Lorem Ipsum</span>',
'non white-list fn not called');
} finally {
delete Object.prototype.dontcallme;
}
assertFalse('Object.prototype.dontcallme should not have been called',
called);
}
function testQuotesInAttributeValue() {
run('<span tItlE =\n\'Quoth the raven, "Nevermore"\'>Lorem Ipsum',
'<span title="Quoth the raven, &quot;Nevermore&quot;">Lorem Ipsum</span>',
'quotes in attr value');
}
function testAttributesNeverMentionedAreDropped() {
run('<b onclick="evil=true">evil</b>', '<b>evil</b>', 'attrs white-listed');
}
function testAttributesNotOverEscaped() {
run('<I TITLE="Foo &AMP; Bar & Baz">/</I>',
'<i title="Foo &amp; Bar &amp; Baz">/</i>',
'attr value not over-escaped');
}
function testTagSpecificRulesTakePrecedence() {
run('<a title=zogberts>Link</a>',
'<a title="&lt;zogberts&gt;">Link</a>',
'tag specific rules take precedence');
}
function testAttributeRejectionLocalized() {
run('<a id=foo href =//evil.org/ title=>Link</a>',
'<a title="&lt;&gt;">Link</a>',
'failure of one attribute does not torpedo others');
}
function testWeirdHtmlRulesFollowedForAttrValues() {
run('<span title= id=>Lorem Ipsum</span>',
'<span title=\"id=\">Lorem Ipsum</span>',
'same as browser on weird values');
}
function testAttributesDisallowedOnCloseTags() {
run('<h1 title="open">Header</h1 title="closed">',
'<h1 title="open">Header</h1>',
'attributes on close tags');
}
function testRoundTrippingOfHtmlSafeAgainstIEBacktickProblems() {
// Introducing a space at the end of an attribute forces IE to quote it when
// turning a DOM into innerHTML which protects against a bunch of problems
// with backticks since IE treats them as attribute value delimiters, allowing
// foo.innerHTML += ...
// to continue to "work" without introducing an XSS vector.
// Adding a space at the end is innocuous since HTML attributes whose values
// are structured content ignore spaces at the beginning or end.
run('<span title="`backtick">*</span>', '<span title="`backtick ">*</span>',
'not round-trippable on IE');
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,254 @@
// Copyright 2013 The Closure Library 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.
goog.provide('goog.html.ScrubberTest');
goog.require('goog.labs.html.scrubber');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.html.ScrubberTest');
var tagWhitelist = goog.object.createSet(
'a', 'b', 'i', 'p', 'font', 'hr', 'br', 'span',
'ol', 'ul', 'li',
'table', 'tr', 'td', 'th', 'tbody',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'img',
'html', 'head', 'body', 'title');
var attrWhitelist = {
// On any element,
'*': {
// allow unfiltered title attributes, and
'title': function(title) { return title; },
// specific dir values.
'dir': function(dir) {
return dir === 'ltr' || dir === 'rtl' ? dir : null;
}
},
// Specifically on <a> elements,
'a': {
// allow an href but verify and rewrite, and
'href': function(href) {
return /^https?:\/\/google\.[a-z]{2,3}\/search\?/.test(href) ?
href.replace(/[^A-Za-z0-9_\-.~:\/?#\[\]@!\$&()*+,;=%]+/,
encodeURIComponent) :
null;
},
// mask the generic title handler for no good reason.
'title': function(title) { return '<' + title + '>'; }
}
};
function run(input, golden, desc) {
var actual = goog.labs.html.scrubber.scrub(
tagWhitelist, attrWhitelist, input);
assertEquals(desc, golden, actual);
}
function testEmptyString() {
run('', '', 'Empty string');
}
function testHelloWorld() {
run('Hello, <b>World</b>!', 'Hello, <b>World</b>!',
'Hello World');
}
function testNoEndTag() {
run('<i>Hello, <b>World!',
'<i>Hello, <b>World!</b></i>',
'Hello World no end tag');
}
function testUnclosedTags() {
run('<html><head><title>Hello, <<World>>!</TITLE>' +
'</head><body><p>Hello,<Br><<World>>!',
'<html><head><title>Hello, <<World>>!</title>' +
'</head><body><p>Hello,<br>&lt;&gt;!</p></body></html>',
'RCDATA content, different case, unclosed tags');
}
function testListInList() {
run('<ul><li>foo</li><ul><li>bar</li></ul></ul>',
'<ul><li>foo</li><li><ul><li>bar</li></ul></li></ul>',
'list in list directly');
}
function testHeaders() {
run('<h1>header</h1>body' +
'<H2>sub-header</h3>sub-body' +
'<h3>sub-sub-</hr>header<hr></hr>sub-sub-body</H4></h2>',
'<h1>header</h1>body' +
'<h2>sub-header</h2>sub-body' +
'<h3>sub-sub-header</h3><hr>sub-sub-body',
'headers');
}
function testListNesting() {
run('<ul><li><ul><li>foo</li></li><ul><li>bar',
'<ul><li><ul><li>foo</li><li><ul><li>bar</li></ul></li></ul></li></ul>',
'list nesting');
}
function testTableNesting() {
run('<table><tbody><tr><td>foo</td><table><tbody><tr><th>bar</table></table>',
'<table><tbody><tr><td>foo</td><td>' +
'<table><tbody><tr><th>bar</th></tr></tbody></table>' +
'</td></tr></tbody></table>',
'table nesting');
}
function testNestingLimit() {
run(goog.string.repeat('<span>', 264) + goog.string.repeat('</span>', 264),
goog.string.repeat('<span>', 256) + goog.string.repeat('</span>', 256),
'264 open spans');
}
function testTableScopes() {
run('<html><head></head><body><p>Hi</p><p>How are you</p>\n' +
'<p><table><tbody><tr>' +
'<td><b><font><font><p>Cell</b></font></font></p>\n</td>' +
'<td><b><font><font><p>Cell</b></font></font></p>\n</td>' +
'</tr></tbody></table></p>\n' +
'<p>x</p></body></html>',
'<html><head></head><body><p>Hi</p><p>How are you</p>\n' +
'<p><table><tbody><tr>' +
'<td><b><font><font></font></font></b><p>Cell</p>\n</td>' +
// The close </p> tag does not close the whole table. +
'<td><b><font><font></font></font></b><p>Cell</p>\n</td>' +
'</tr></tbody></table></p>\n' +
'<p>x</p></body></html>',
'Table Scopes');
}
function testConcatSafe() {
run('<<applet>script<applet>>alert(1337)<<!-- -->/script<?...?>>',
'&lt;script&gt;alert(1337)&lt;/script&gt;',
'Concat safe');
}
function testPrototypeMembersDoNotInfectTables() {
// Constructor is all lower-case so will survive tag name
// normalization.
run('<constructor>Foo</constructor>', 'Foo',
'Object.prototype members');
}
function testGenericAttributesAllowed() {
run('<span title=howdy></span>', '<span title="howdy"></span>',
'generic attrs allowed');
}
function testValueWhitelisting() {
run('<span dir=\'ltr\'>LTR</span><span dir=\'evil\'>Evil</span>',
'<span dir="ltr">LTR</span><span>Evil</span>',
'value whitelisted');
}
function testAttributeNormalization() {
run('<a href="http://google.com/search?q=tests suxor&hl=en">Click</a>',
'<a href="http://google.com/search?q=tests%20suxor&amp;hl=en">Click</a>',
'URL normalized');
}
function testTagSpecificityOfAttributeFiltering() {
run('<img href="http://google.com/search?q=tests+suxor">',
'<img>',
'href blocked on img');
}
function testTagSpecificAttributeFiltering() {
run('<a href="http://google.evil.com/search?q=tests suxor">Unclicky</a>',
'<a>Unclicky</a>',
'bad href value blocked');
}
function testNonWhitelistFunctionsNotCalled() {
var called = false;
Object.prototype.dontcallme = function() {
called = true;
return 'dontcallme was called despite being on the prototype';
};
try {
run('<span dontcallme="I\'ll call you">Lorem Ipsum',
'<span>Lorem Ipsum</span>',
'non white-list fn not called');
} finally {
delete Object.prototype.dontcallme;
}
assertFalse('Object.prototype.dontcallme should not have been called',
called);
}
function testQuotesInAttributeValue() {
run('<span tItlE =\n\'Quoth the raven, "Nevermore"\'>Lorem Ipsum',
'<span title="Quoth the raven, &quot;Nevermore&quot;">Lorem Ipsum</span>',
'quotes in attr value');
}
function testAttributesNeverMentionedAreDropped() {
run('<b onclick="evil=true">evil</b>', '<b>evil</b>', 'attrs white-listed');
}
function testAttributesNotOverEscaped() {
run('<I TITLE="Foo &AMP; Bar & Baz">/</I>',
'<i title="Foo &amp; Bar &amp; Baz">/</i>',
'attr value not over-escaped');
}
function testTagSpecificRulesTakePrecedence() {
run('<a title=zogberts>Link</a>',
'<a title="&lt;zogberts&gt;">Link</a>',
'tag specific rules take precedence');
}
function testAttributeRejectionLocalized() {
run('<a id=foo href =//evil.org/ title=>Link</a>',
'<a title="&lt;&gt;">Link</a>',
'failure of one attribute does not torpedo others');
}
function testWeirdHtmlRulesFollowedForAttrValues() {
run('<span title= id=>Lorem Ipsum</span>',
'<span title=\"id=\">Lorem Ipsum</span>',
'same as browser on weird values');
}
function testAttributesDisallowedOnCloseTags() {
run('<h1 title="open">Header</h1 title="closed">',
'<h1 title="open">Header</h1>',
'attributes on close tags');
}
function testRoundTrippingOfHtmlSafeAgainstIEBacktickProblems() {
// Introducing a space at the end of an attribute forces IE to quote it when
// turning a DOM into innerHTML which protects against a bunch of problems
// with backticks since IE treats them as attribute value delimiters, allowing
// foo.innerHTML += ...
// to continue to "work" without introducing an XSS vector.
// Adding a space at the end is innocuous since HTML attributes whose values
// are structured content ignore spaces at the beginning or end.
run('<span title="`backtick">*</span>', '<span title="`backtick ">*</span>',
'not round-trippable on IE');
}
@@ -0,0 +1,261 @@
// Copyright 2013 The Closure Library 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 List format and gender decision library with locale support.
*
* ListFormat takes an array or a var_arg of objects and generates a user
* friendly list in a locale-sensitive way (i.e. "red, green, and blue").
*
* GenderInfo can be used to determine the gender of a list of items,
* depending on the gender of all items in the list.
*
* In English, lists of items don't really have gender, and in fact few things
* have gender. But the idea is this:
* - for a list of "male items" (think "John, Steve") you use "they"
* - for "Marry, Ann" (all female) you might have a "feminine" form of "they"
* - and yet another form for mixed lists ("John, Marry") or undetermined
* (when you don't know the gender of the items, or when they are neuter)
*
* For example in Greek "they" will be translated as "αυτοί" for masculin,
* "αυτές" for feminin, and "αυτά" for neutral/undetermined.
* (it is in fact more complicated than that, as weak/strong forms and case
* also matter, see http://en.wiktionary.org/wiki/Appendix:Greek_pronouns)
*
*/
goog.provide('goog.labs.i18n.GenderInfo');
goog.provide('goog.labs.i18n.GenderInfo.Gender');
goog.provide('goog.labs.i18n.ListFormat');
goog.require('goog.asserts');
goog.require('goog.labs.i18n.ListFormatSymbols');
/**
* ListFormat provides a method to format a list/array of objects to a string,
* in a user friendly way and in a locale sensitive manner.
* If the objects are not strings, toString is called to convert them.
* The constructor initializes the object based on the locale data from
* the current goog.labs.i18n.ListFormatSymbols.
*
* Similar to the ICU4J class com.ibm.icu.text.ListFormatter:
* http://icu-project.org/apiref/icu4j/com/ibm/icu/text/ListFormatter.html
* @constructor
* @final
*/
goog.labs.i18n.ListFormat = function() {
/**
* String for lists of exactly two items, containing {0} for the first,
* and {1} for the second.
* For instance '{0} and {1}' will give 'black and white'.
* @private {string}
*
* Example: for "black and white" the pattern is "{0} and {1}"
* While for a longer list we have "cyan, magenta, yellow, and black"
* Think "{0} start {1} middle {2} middle {3} end {4}"
* The last pattern is "{0}, and {1}." Note the comma before "and".
* So the "Two" pattern can be different than Start/Middle/End ones.
*/
this.listTwoPattern_ = goog.labs.i18n.ListFormatSymbols.LIST_TWO;
/**
* String for the start of a list items, containing {0} for the first,
* and {1} for the rest.
* @private {string}
*/
this.listStartPattern_ = goog.labs.i18n.ListFormatSymbols.LIST_START;
/**
* String for the start of a list items, containing {0} for the first part
* of the list, and {1} for the rest of the list.
* @private {string}
*/
this.listMiddlePattern_ = goog.labs.i18n.ListFormatSymbols.LIST_MIDDLE;
/**
* String for the end of a list items, containing {0} for the first part
* of the list, and {1} for the last item.
*
* This is how start/middle/end come together:
* start = '{0}, {1}' middle = '{0}, {1}', end = '{0}, and {1}'
* will result in the typical English list: 'one, two, three, and four'
* There are languages where the patterns are more complex than
* '{1} someText {1}' and the start pattern is different than the middle one.
*
* @private {string}
*/
this.listEndPattern_ = goog.labs.i18n.ListFormatSymbols.LIST_END;
};
/**
* Replaces the {0} and {1} placeholders in a pattern with the first and
* the second parameter respectively, and returns the result.
* It is a helper function for goog.labs.i18n.ListFormat.format.
*
* @param {string} pattern used for formatting.
* @param {string} first object to add to list.
* @param {string} second object to add to list.
* @return {string} The formatted list string.
* @private
*/
goog.labs.i18n.ListFormat.prototype.patternBasedJoinTwoStrings_ =
function(pattern, first, second) {
return pattern.replace('{0}', first).replace('{1}', second);
};
/**
* Formats an array of strings into a string.
* It is a user facing, locale-aware list (i.e. 'red, green, and blue').
*
* @param {!Array<string|number>} items Items to format.
* @return {string} The items formatted into a string, as a list.
*/
goog.labs.i18n.ListFormat.prototype.format = function(items) {
var count = items.length;
switch (count) {
case 0:
return '';
case 1:
return String(items[0]);
case 2:
return this.patternBasedJoinTwoStrings_(this.listTwoPattern_,
String(items[0]), String(items[1]));
}
var result = this.patternBasedJoinTwoStrings_(this.listStartPattern_,
String(items[0]), String(items[1]));
for (var i = 2; i < count - 1; ++i) {
result = this.patternBasedJoinTwoStrings_(this.listMiddlePattern_,
result, String(items[i]));
}
return this.patternBasedJoinTwoStrings_(this.listEndPattern_,
result, String(items[count - 1]));
};
/**
* GenderInfo provides a method to determine the gender of a list/array
* of objects when one knows the gender of each item of the list.
* It does this in a locale sensitive manner.
* The constructor initializes the object based on the locale data from
* the current goog.labs.i18n.ListFormatSymbols.
*
* Similar to the ICU4J class com.icu.util.GenderInfo:
* http://icu-project.org/apiref/icu4j/com/ibm/icu/util/GenderInfo.html
* @constructor
* @final
*/
goog.labs.i18n.GenderInfo = function() {
/**
* Stores the language-aware mode of determining the gender of a list.
* @private {goog.labs.i18n.GenderInfo.ListGenderStyle_}
*/
this.listGenderStyle_ = goog.labs.i18n.ListFormatSymbols.GENDER_STYLE;
};
/**
* Enumeration for the possible ways to generate list genders.
* Indicates the category for the locale.
* This only affects gender for lists more than one. For lists of 1 item,
* the gender of the list always equals the gender of that sole item.
* This is for internal use, matching ICU.
* @enum {number}
* @private
*/
goog.labs.i18n.GenderInfo.ListGenderStyle_ = {
NEUTRAL: 0,
MIXED_NEUTRAL: 1,
MALE_TAINTS: 2
};
/**
* Enumeration for the possible gender values.
* Gender: OTHER means either the information is unavailable,
* or the person has declined to state MALE or FEMALE.
* @enum {number}
*/
goog.labs.i18n.GenderInfo.Gender = {
MALE: 0,
FEMALE: 1,
OTHER: 2
};
/**
* Determines the overal gender of a list based on the gender of all the list
* items, in a locale-aware way.
* @param {!Array<!goog.labs.i18n.GenderInfo.Gender>} genders An array of
* genders, will give the gender of the list.
* @return {goog.labs.i18n.GenderInfo.Gender} Get the gender of the list.
*/
goog.labs.i18n.GenderInfo.prototype.getListGender = function(genders) {
var Gender = goog.labs.i18n.GenderInfo.Gender;
var count = genders.length;
if (count == 0) {
return Gender.OTHER; // degenerate case
}
if (count == 1) {
return genders[0]; // degenerate case
}
switch (this.listGenderStyle_) {
case goog.labs.i18n.GenderInfo.ListGenderStyle_.NEUTRAL:
return Gender.OTHER;
case goog.labs.i18n.GenderInfo.ListGenderStyle_.MIXED_NEUTRAL:
var hasFemale = false;
var hasMale = false;
for (var i = 0; i < count; ++i) {
switch (genders[i]) {
case Gender.FEMALE:
if (hasMale) {
return Gender.OTHER;
}
hasFemale = true;
break;
case Gender.MALE:
if (hasFemale) {
return Gender.OTHER;
}
hasMale = true;
break;
case Gender.OTHER:
return Gender.OTHER;
default: // Should never happen, but just in case
goog.asserts.assert(false,
'Invalid genders[' + i + '] = ' + genders[i]);
return Gender.OTHER;
}
}
return hasMale ? Gender.MALE : Gender.FEMALE;
case goog.labs.i18n.GenderInfo.ListGenderStyle_.MALE_TAINTS:
for (var i = 0; i < count; ++i) {
if (genders[i] != Gender.FEMALE) {
return Gender.MALE;
}
}
return Gender.FEMALE;
default:
return Gender.OTHER;
}
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.i18n.ListFormat
</title>
<meta charset="utf-8" />
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.i18n.ListFormatTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,324 @@
// Copyright 2013 The Closure Library 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.
goog.provide('goog.labs.i18n.ListFormatTest');
goog.setTestOnly('goog.labs.i18n.ListFormatTest');
goog.require('goog.labs.i18n.GenderInfo');
goog.require('goog.labs.i18n.ListFormat');
goog.require('goog.labs.i18n.ListFormatSymbols');
goog.require('goog.labs.i18n.ListFormatSymbols_el');
goog.require('goog.labs.i18n.ListFormatSymbols_en');
goog.require('goog.labs.i18n.ListFormatSymbols_fr');
goog.require('goog.labs.i18n.ListFormatSymbols_ml');
goog.require('goog.labs.i18n.ListFormatSymbols_zu');
goog.require('goog.testing.jsunit');
function setUp() {
// Always switch back to English on startup.
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
}
function testListFormatterArrayDirect() {
var fmt = new goog.labs.i18n.ListFormat();
assertEquals(
'One',
fmt.format(['One'])
);
assertEquals(
'One and Two',
fmt.format(['One', 'Two'])
);
assertEquals(
'One, Two, and Three',
fmt.format(['One', 'Two', 'Three'])
);
assertEquals(
'One, Two, Three, Four, Five, and Six',
fmt.format(['One', 'Two', 'Three', 'Four', 'Five', 'Six'])
);
}
function testListFormatterArrayIndirect() {
var fmt = new goog.labs.i18n.ListFormat();
var items = [];
items.push('One');
assertEquals('One', fmt.format(items));
items.push('Two');
assertEquals('One and Two', fmt.format(items));
items.push('Three');
assertEquals('One, Two, and Three', fmt.format(items));
items.push('Four');
items.push('Five');
items.push('Six');
assertEquals('One, Two, Three, Four, Five, and Six', fmt.format(items));
}
function testListFormatterFrench() {
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_fr;
var fmt = new goog.labs.i18n.ListFormat();
assertEquals(
'One',
fmt.format(['One'])
);
assertEquals(
'One et Two',
fmt.format(['One', 'Two'])
);
assertEquals(
'One, Two et Three',
fmt.format(['One', 'Two', 'Three'])
);
assertEquals(
'One, Two, Three, Four, Five et Six',
fmt.format(['One', 'Two', 'Three', 'Four', 'Five', 'Six'])
);
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
}
// Malayalam and Zulu are the only two locales with pathers
// different than '{0} sometext {1}'
function testListFormatterSpecialLanguages() {
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_ml;
var fmt_ml = new goog.labs.i18n.ListFormat();
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_zu;
var fmt_zu = new goog.labs.i18n.ListFormat();
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
// Only the end pattern is special with Malayalam
// Escaped for safety, the string is 'One, Two, Three എന്നിവ'
assertEquals('One, Two, Three \u0D0E\u0D28\u0D4D\u0D28\u0D3F\u0D35',
fmt_ml.format(['One', 'Two', 'Three']));
// Only the two items pattern is special with Zulu
assertEquals('I-One ne-Two', fmt_zu.format(['One', 'Two']));
}
function testVariousObjectTypes() {
var fmt = new goog.labs.i18n.ListFormat();
var booleanObject = new Boolean(1);
var arrayObject = ['black', 'white'];
// Not sure how "flaky" this is. Firefox and Chrome give the same results,
// but I am not sure if the JavaScript standard specifies exactly what
// Array toString does, for instance.
assertEquals(
'One, black,white, 42, true, and Five',
fmt.format(['One', arrayObject, 42, booleanObject, 'Five'])
);
}
function testListGendersNeutral() {
var Gender = goog.labs.i18n.GenderInfo.Gender;
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
var listGen = new goog.labs.i18n.GenderInfo();
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE]));
assertEquals(Gender.FEMALE, listGen.getListGender([Gender.FEMALE]));
assertEquals(Gender.OTHER, listGen.getListGender([Gender.OTHER]));
assertEquals(Gender.OTHER, listGen.getListGender([Gender.MALE, Gender.MALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.FEMALE, Gender.FEMALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.OTHER, Gender.OTHER]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.MALE, Gender.OTHER]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.OTHER, Gender.MALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.MALE, Gender.FEMALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.FEMALE, Gender.MALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.OTHER, Gender.FEMALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.FEMALE, Gender.OTHER]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.MALE, Gender.FEMALE, Gender.OTHER]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.MALE, Gender.OTHER, Gender.FEMALE]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.FEMALE, Gender.MALE, Gender.OTHER]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.FEMALE, Gender.OTHER, Gender.MALE]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.OTHER, Gender.MALE, Gender.FEMALE]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.OTHER, Gender.FEMALE, Gender.MALE]));
}
function testListGendersMaleTaints() {
var Gender = goog.labs.i18n.GenderInfo.Gender;
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_fr;
var listGen = new goog.labs.i18n.GenderInfo();
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE]));
assertEquals(Gender.FEMALE, listGen.getListGender([Gender.FEMALE]));
assertEquals(Gender.OTHER, listGen.getListGender([Gender.OTHER]));
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE, Gender.MALE]));
assertEquals(
Gender.FEMALE, listGen.getListGender([Gender.FEMALE, Gender.FEMALE]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.OTHER, Gender.OTHER]));
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE, Gender.OTHER]));
assertEquals(Gender.MALE, listGen.getListGender([Gender.OTHER, Gender.MALE]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.MALE, Gender.FEMALE]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.FEMALE, Gender.MALE]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.OTHER, Gender.FEMALE]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.FEMALE, Gender.OTHER]));
assertEquals(
Gender.MALE,
listGen.getListGender([Gender.MALE, Gender.FEMALE, Gender.OTHER]));
assertEquals(
Gender.MALE,
listGen.getListGender([Gender.MALE, Gender.OTHER, Gender.FEMALE]));
assertEquals(
Gender.MALE,
listGen.getListGender([Gender.FEMALE, Gender.MALE, Gender.OTHER]));
assertEquals(
Gender.MALE,
listGen.getListGender([Gender.FEMALE, Gender.OTHER, Gender.MALE]));
assertEquals(
Gender.MALE,
listGen.getListGender([Gender.OTHER, Gender.MALE, Gender.FEMALE]));
assertEquals(
Gender.MALE,
listGen.getListGender([Gender.OTHER, Gender.FEMALE, Gender.MALE]));
}
function testListGendersMixedNeutral() {
var Gender = goog.labs.i18n.GenderInfo.Gender;
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_el;
var listGen = new goog.labs.i18n.GenderInfo();
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE]));
assertEquals(Gender.FEMALE, listGen.getListGender([Gender.FEMALE]));
assertEquals(Gender.OTHER, listGen.getListGender([Gender.OTHER]));
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE, Gender.MALE]));
assertEquals(
Gender.FEMALE, listGen.getListGender([Gender.FEMALE, Gender.FEMALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.OTHER, Gender.OTHER]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.MALE, Gender.OTHER]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.OTHER, Gender.MALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.MALE, Gender.FEMALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.FEMALE, Gender.MALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.OTHER, Gender.FEMALE]));
assertEquals(
Gender.OTHER, listGen.getListGender([Gender.FEMALE, Gender.OTHER]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.MALE, Gender.FEMALE, Gender.OTHER]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.MALE, Gender.OTHER, Gender.FEMALE]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.FEMALE, Gender.MALE, Gender.OTHER]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.FEMALE, Gender.OTHER, Gender.MALE]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.OTHER, Gender.MALE, Gender.FEMALE]));
assertEquals(
Gender.OTHER,
listGen.getListGender([Gender.OTHER, Gender.FEMALE, Gender.MALE]));
}
function testListGendersVariousCallTypes() {
var Gender = goog.labs.i18n.GenderInfo.Gender;
// Using French because with English the results are mostly Gender.OTHER
// so we can detect fewer problems
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_fr;
var listGen = new goog.labs.i18n.GenderInfo();
goog.labs.i18n.ListFormatSymbols = goog.labs.i18n.ListFormatSymbols_en;
// Anynymous Arrays
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE]));
assertEquals(Gender.FEMALE, listGen.getListGender([Gender.FEMALE]));
assertEquals(Gender.OTHER, listGen.getListGender([Gender.OTHER]));
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE, Gender.MALE]));
assertEquals(
Gender.FEMALE, listGen.getListGender([Gender.FEMALE, Gender.FEMALE]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.OTHER, Gender.OTHER]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.MALE, Gender.FEMALE]));
assertEquals(Gender.MALE, listGen.getListGender([Gender.MALE, Gender.OTHER]));
assertEquals(
Gender.MALE, listGen.getListGender([Gender.FEMALE, Gender.OTHER]));
// Arrays
var arrayM = [Gender.MALE];
var arrayF = [Gender.FEMALE];
var arrayO = [Gender.OTHER];
var arrayMM = [Gender.MALE, Gender.MALE];
var arrayFF = [Gender.FEMALE, Gender.FEMALE];
var arrayOO = [Gender.OTHER, Gender.OTHER];
var arrayMF = [Gender.MALE, Gender.FEMALE];
var arrayMO = [Gender.MALE, Gender.OTHER];
var arrayFO = [Gender.FEMALE, Gender.OTHER];
assertEquals(Gender.MALE, listGen.getListGender(arrayM));
assertEquals(Gender.FEMALE, listGen.getListGender(arrayF));
assertEquals(Gender.OTHER, listGen.getListGender(arrayO));
assertEquals(Gender.MALE, listGen.getListGender(arrayMM));
assertEquals(Gender.FEMALE, listGen.getListGender(arrayFF));
assertEquals(Gender.MALE, listGen.getListGender(arrayOO));
assertEquals(Gender.MALE, listGen.getListGender(arrayMF));
assertEquals(Gender.MALE, listGen.getListGender(arrayMO));
assertEquals(Gender.MALE, listGen.getListGender(arrayFO));
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
// Copyright 2014 The Closure Library 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 Utilities for working with ES6 iterables.
* Note that this file is written ES5-only.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/The_Iterator_protocol
*/
goog.module('goog.labs.iterable');
/**
* Get the iterator for an iterable.
* @param {!Iterable<VALUE>} iterable
* @return {!Iterator<VALUE>}
* @template VALUE
*/
exports.getIterator = function(iterable) {
return iterable[goog.global.Symbol.iterator]();
};
/**
* Call a function with every value of an iterable.
*
* Warning: this function will never halt if given an iterable that
* is never exhausted.
*
* @param {!function(VALUE): void} f
* @param {!Iterable<VALUE>} iterable
* @template VALUE
*/
exports.forEach = function(f, iterable) {
var iterator = exports.getIterator(iterable);
while (true) {
var next = iterator.next();
if (next.done) {
return;
}
f(next.value);
}
};
/**
* Maps the values of one iterable to create another iterable.
*
* When next() is called on the returned iterable, it will call the given
* function {@code f} with the next value of the given iterable
* {@code iterable} until the given iterable is exhausted.
*
* @param {!function(this: THIS, VALUE): RESULT} f
* @param {!Iterable<VALUE>} iterable
* @return {!Iterable<RESULT>} The created iterable that gives the mapped
* values.
* @template THIS, VALUE, RESULT
*/
exports.map = function(f, iterable) {
return new FactoryIterable(function() {
var iterator = exports.getIterator(iterable);
return new MapIterator(f, iterator);
});
};
/**
* Helper class for {@code map}.
* @param {!function(VALUE): RESULT} f
* @param {!Iterator<VALUE>} iterator
* @constructor
* @implements {Iterator<RESULT>}
* @template VALUE, RESULT
*/
var MapIterator = function(f, iterator) {
/** @private */
this.func_ = f;
/** @private */
this.iterator_ = iterator;
};
/**
* @override
*/
MapIterator.prototype.next = function() {
var nextObj = this.iterator_.next();
if (nextObj.done) {
return {done: true, value: undefined};
}
var mappedValue = this.func_(nextObj.value);
return {
done: false,
value: mappedValue
};
};
/**
* Helper class to create an iterable with a given iterator factory.
* @param {function():!Iterator<VALUE>} iteratorFactory
* @constructor
* @implements {Iterable<VALUE>}
* @template VALUE
*/
var FactoryIterable = function(iteratorFactory) {
/**
* @private
*/
this.iteratorFactory_ = iteratorFactory;
};
// TODO(nnaze): For now, this section is not run if Symbol is not defined,
// since goog.global.Symbol.iterator will not be defined below.
// Determine best course of action if "Symbol" is not available.
if (goog.global.Symbol) {
/**
* @return {!Iterator<VALUE>}
*/
FactoryIterable.prototype[goog.global.Symbol.iterator] = function() {
return this.iteratorFactory_();
};
}
@@ -0,0 +1,146 @@
// Copyright 2014 The Closure Library 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 Tests for goog.labs.iterable
*/
goog.module('goog.labs.iterableTest');
goog.module.declareTestMethods();
goog.setTestOnly();
goog.require('goog.testing.jsunit');
var iterable = goog.require('goog.labs.iterable');
var recordFunction = goog.require('goog.testing.recordFunction');
/**
* Create an iterator starting at "start" and increments up to
* (but not including) "stop".
*/
function createRangeIterator(start, stop) {
var value = start;
var next = function() {
if (value < stop) {
return {
value: value++,
done: false
};
}
return {
value: undefined,
done: true
};
};
return {
next: next
};
}
function createRangeIterable(start, stop) {
var obj = {};
// Refer to goog.global['Symbol'] because otherwise this
// is a parse error in earlier IEs.
obj[goog.global['Symbol'].iterator] = function() {
return createRangeIterator(start, stop);
};
return obj;
}
function isSymbolDefined() {
return !!goog.global['Symbol'];
}
exports.testCreateRangeIterable = function() {
// Do not run if Symbol does not exist in this browser.
if (!isSymbolDefined()) {
return;
}
var rangeIterator = createRangeIterator(0, 3);
for (var i = 0; i < 3; i++) {
assertObjectEquals({
value: i,
done: false
}, rangeIterator.next());
}
for (var i = 0; i < 3; i++) {
assertObjectEquals({
value: undefined,
done: true
}, rangeIterator.next());
}
};
exports.testForEach = function() {
// Do not run if Symbol does not exist in this browser.
if (!isSymbolDefined()) {
return;
}
var range = createRangeIterable(0, 3);
var callback = recordFunction();
iterable.forEach(callback, range, self);
callback.assertCallCount(3);
var calls = callback.getCalls();
for (var i = 0; i < calls.length; i++) {
var call = calls[i];
assertArrayEquals([i], call.getArguments());
}
};
exports.testMap = function() {
// Do not run if Symbol does not exist in this browser.
if (!isSymbolDefined()) {
return;
}
var range = createRangeIterable(0, 3);
function addTwo(i) {
return i + 2;
}
var newIterable = iterable.map(addTwo, range);
var newIterator = iterable.getIterator(newIterable);
var nextObj = newIterator.next();
assertEquals(2, nextObj.value);
assertFalse(nextObj.done);
nextObj = newIterator.next();
assertEquals(3, nextObj.value);
assertFalse(nextObj.done);
nextObj = newIterator.next();
assertEquals(4, nextObj.value);
assertFalse(nextObj.done);
// Check that the iterator repeatedly signals done.
for (var i = 0; i < 3; i++) {
nextObj = newIterator.next();
assertUndefined(nextObj.value);
assertTrue(nextObj.done);
}
};
@@ -0,0 +1,861 @@
// Copyright 2012 The Closure Library 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 Provides a mocking framework in Closure to make unit tests easy
* to write and understand. The methods provided here can be used to replace
* implementations of existing objects with 'mock' objects to abstract out
* external services and dependencies thereby isolating the code under test.
* Apart from mocking, methods are also provided to just monitor calls to an
* object (spying) and returning specific values for some or all the inputs to
* methods (stubbing).
*
* Design doc : http://go/closuremock
*
*/
goog.provide('goog.labs.mock');
goog.provide('goog.labs.mock.VerificationError');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug');
goog.require('goog.debug.Error');
goog.require('goog.functions');
goog.require('goog.object');
/**
* Mocks a given object or class.
*
* @param {!Object} objectOrClass An instance or a constructor of a class to be
* mocked.
* @return {!Object} The mocked object.
*/
goog.labs.mock.mock = function(objectOrClass) {
// Go over properties of 'objectOrClass' and create a MockManager to
// be used for stubbing out calls to methods.
var mockObjectManager = new goog.labs.mock.MockObjectManager_(objectOrClass);
var mockedObject = mockObjectManager.getMockedItem();
goog.asserts.assertObject(mockedObject);
return /** @type {!Object} */ (mockedObject);
};
/**
* Mocks a given function.
*
* @param {!Function} func A function to be mocked.
* @return {!Function} The mocked function.
*/
goog.labs.mock.mockFunction = function(func) {
var mockFuncManager = new goog.labs.mock.MockFunctionManager_(func);
var mockedFunction = mockFuncManager.getMockedItem();
goog.asserts.assertFunction(mockedFunction);
return /** @type {!Function} */ (mockedFunction);
};
/**
* Spies on a given object.
*
* @param {!Object} obj The object to be spied on.
* @return {!Object} The spy object.
*/
goog.labs.mock.spy = function(obj) {
// Go over properties of 'obj' and create a MockSpyManager_ to
// be used for spying on calls to methods.
var mockSpyManager = new goog.labs.mock.MockSpyManager_(obj);
var spyObject = mockSpyManager.getMockedItem();
goog.asserts.assert(spyObject);
return spyObject;
};
/**
* Returns an object that can be used to verify calls to specific methods of a
* given mock.
*
* @param {!Object} obj The mocked object.
* @return {!Object} The verifier.
*/
goog.labs.mock.verify = function(obj) {
return obj.$callVerifier;
};
/**
* Returns a name to identify a function. Named functions return their names,
* unnamed functions return a string of the form '#anonymous{ID}' where ID is
* a unique identifier for each anonymous function.
* @private
* @param {!Function} func The function.
* @return {string} The function name.
*/
goog.labs.mock.getFunctionName_ = function(func) {
var funcName = goog.debug.getFunctionName(func);
if (funcName == '' || funcName == '[Anonymous]') {
funcName = '#anonymous' + goog.labs.mock.getUid(func);
}
return funcName;
};
/**
* Returns a nicely formatted, readble representation of a method call.
* @private
* @param {string} methodName The name of the method.
* @param {Array<?>=} opt_args The method arguments.
* @return {string} The string representation of the method call.
*/
goog.labs.mock.formatMethodCall_ = function(methodName, opt_args) {
opt_args = opt_args || [];
opt_args = goog.array.map(opt_args, function(arg) {
if (goog.isFunction(arg)) {
var funcName = goog.labs.mock.getFunctionName_(arg);
return '<function ' + funcName + '>';
} else {
var isObjectWithClass = goog.isObject(arg) &&
!goog.isFunction(arg) && !goog.isArray(arg) &&
arg.constructor != Object;
if (isObjectWithClass) {
return arg.toString();
}
return goog.labs.mock.formatValue_(arg);
}
});
return methodName + '(' + opt_args.join(', ') + ')';
};
/**
* An array to store objects for unique id generation.
* @private
* @type {!Array<!Object>}
*/
goog.labs.mock.uid_ = [];
/**
* A unique Id generator that does not modify the object.
* @param {Object!} obj The object whose unique ID we want to generate.
* @return {number} an unique id for the object.
*/
goog.labs.mock.getUid = function(obj) {
var index = goog.array.indexOf(goog.labs.mock.uid_, obj);
if (index == -1) {
index = goog.labs.mock.uid_.length;
goog.labs.mock.uid_.push(obj);
}
return index;
};
/**
* This is just another implementation of goog.debug.deepExpose with a more
* compact format.
* @private
* @param {*} obj The object whose string representation will be returned.
* @param {boolean=} opt_id Whether to include the id of objects or not.
* Defaults to true.
* @return {string} The string representation of the object.
*/
goog.labs.mock.formatValue_ = function(obj, opt_id) {
var id = goog.isDef(opt_id) ? opt_id : true;
var previous = [];
var output = [];
var helper = function(obj) {
var indentMultiline = function(output) {
return output.replace(/\n/g, '\n');
};
/** @preserveTry */
try {
if (!goog.isDef(obj)) {
output.push('undefined');
} else if (goog.isNull(obj)) {
output.push('NULL');
} else if (goog.isString(obj)) {
output.push('"' + indentMultiline(obj) + '"');
} else if (goog.isFunction(obj)) {
var funcName = goog.labs.mock.getFunctionName_(obj);
output.push('<function ' + funcName + '>');
} else if (goog.isObject(obj)) {
if (goog.array.contains(previous, obj)) {
if (id) {
output.push('<recursive/dupe obj_' +
goog.labs.mock.getUid(obj) + '>');
} else {
output.push('<recursive/dupe>');
}
} else {
previous.push(obj);
output.push('{');
var inner_obj = [];
for (var x in obj) {
output.push(' ');
output.push('"' + x + '"' + ':');
helper(obj[x]);
}
if (id) {
output.push(' _id:' + goog.labs.mock.getUid(obj));
}
output.push('}');
}
} else {
output.push(obj);
}
} catch (e) {
output.push('*** ' + e + ' ***');
}
};
helper(obj);
return output.join('').replace(/"closure_uid_\d+"/g, '_id')
.replace(/{ /g, '{');
};
/**
* Error thrown when verification failed.
*
* @param {Array<!goog.labs.mock.MethodBinding_>} recordedCalls
* The recorded calls that didn't match the expectation.
* @param {!string} methodName The expected method call.
* @param {!Array<?>} args The expected arguments.
* @constructor
* @extends {goog.debug.Error}
* @final
*/
goog.labs.mock.VerificationError = function(recordedCalls, methodName, args) {
var msg = goog.labs.mock.VerificationError.getVerificationErrorMsg_(
recordedCalls, methodName, args);
goog.labs.mock.VerificationError.base(this, 'constructor', msg);
};
goog.inherits(goog.labs.mock.VerificationError, goog.debug.Error);
/** @override */
goog.labs.mock.VerificationError.prototype.name = 'VerificationError';
/**
* This array contains the name of the functions that are part of the base
* Object prototype.
* Basically a copy of goog.object.PROTOTYPE_FIELDS_.
* @const
* @type {!Array<string>}
* @private
*/
goog.labs.mock.PROTOTYPE_FIELDS_ = [
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf'
];
/**
* Constructs a descriptive error message for an expected method call.
* @private
* @param {Array<!goog.labs.mock.MethodBinding_>} recordedCalls
* The recorded calls that didn't match the expectation.
* @param {!string} methodName The expected method call.
* @param {!Array<?>} args The expected arguments.
* @return {string} The error message.
*/
goog.labs.mock.VerificationError.getVerificationErrorMsg_ =
function(recordedCalls, methodName, args) {
recordedCalls = goog.array.filter(recordedCalls, function(binding) {
return binding.getMethodName() == methodName;
});
var expected = goog.labs.mock.formatMethodCall_(methodName, args);
var msg = '\nExpected: ' + expected.toString();
msg += '\nRecorded: ';
if (recordedCalls.length > 0) {
msg += recordedCalls.join(',\n ');
} else {
msg += 'No recorded calls';
}
return msg;
};
/**
* Base class that provides basic functionality for creating, adding and
* finding bindings, offering an executor method that is called when a call to
* the stub is made, an array to hold the bindings and the mocked item, among
* other things.
*
* @constructor
* @struct
* @private
*/
goog.labs.mock.MockManager_ = function() {
/**
* Proxies the methods for the mocked object or class to execute the stubs.
* @type {!Object}
* @protected
*/
this.mockedItem = {};
/**
* A reference to the object or function being mocked.
* @type {Object|Function}
* @protected
*/
this.mockee = null;
/**
* Holds the stub bindings established so far.
* @protected
*/
this.methodBindings = [];
/**
* Holds a reference to the binder used to define stubs.
* @protected
*/
this.$stubBinder = null;
/**
* Record method calls with no stub definitions.
* @type {!Array<!goog.labs.mock.MethodBinding_>}
* @private
*/
this.callRecords_ = [];
};
/**
* Handles the first step in creating a stub, returning a stub-binder that
* is later used to bind a stub for a method.
*
* @param {string} methodName The name of the method being bound.
* @param {...*} var_args The arguments to the method.
* @return {!goog.labs.mock.StubBinder_} The stub binder.
* @private
*/
goog.labs.mock.MockManager_.prototype.handleMockCall_ =
function(methodName, var_args) {
var args = goog.array.slice(arguments, 1);
return new goog.labs.mock.StubBinder_(this, methodName, args);
};
/**
* Returns the mock object. This should have a stubbed method for each method
* on the object being mocked.
*
* @return {!Object|!Function} The mock object.
*/
goog.labs.mock.MockManager_.prototype.getMockedItem = function() {
return this.mockedItem;
};
/**
* Adds a binding for the method name and arguments to be stubbed.
*
* @param {?string} methodName The name of the stubbed method.
* @param {!Array<?>} args The arguments passed to the method.
* @param {!Function} func The stub function.
*
*/
goog.labs.mock.MockManager_.prototype.addBinding =
function(methodName, args, func) {
var binding = new goog.labs.mock.MethodBinding_(methodName, args, func);
this.methodBindings.push(binding);
};
/**
* Returns a stub, if defined, for the method name and arguments passed in.
* If there are multiple stubs for this method name and arguments, then
* the first one is returned and removed from the list.
*
* @param {string} methodName The name of the stubbed method.
* @param {!Array<?>} args The arguments passed to the method.
* @return {Function} The stub function or undefined.
* @protected
*/
goog.labs.mock.MockManager_.prototype.getNextBinding =
function(methodName, args) {
var first = -1;
var count = 0;
var stub = null;
goog.array.forEach(this.methodBindings, function(binding, i) {
if (binding.matches(methodName, args, false /* isVerification */)) {
count++;
if (goog.isNull(stub)) {
first = i;
stub = binding;
}
}
});
if (count > 1) {
goog.array.removeAt(this.methodBindings, first);
}
return stub && stub.getStub();
};
/**
* Returns a stub, if defined, for the method name and arguments passed in as
* parameters.
*
* @param {string} methodName The name of the stubbed method.
* @param {!Array<?>} args The arguments passed to the method.
* @return {Function} The stub function or undefined.
* @protected
*/
goog.labs.mock.MockManager_.prototype.getExecutor = function(methodName, args) {
return this.getNextBinding(methodName, args);
};
/**
* Looks up the list of stubs defined on the mock object and executes the
* function associated with that stub.
*
* @param {string} methodName The name of the method to execute.
* @param {...*} var_args The arguments passed to the method.
* @return {*} Value returned by the stub function.
* @protected
*/
goog.labs.mock.MockManager_.prototype.executeStub =
function(methodName, var_args) {
var args = goog.array.slice(arguments, 1);
// Record this call
this.recordCall_(methodName, args);
var func = this.getExecutor(methodName, args);
if (func) {
return func.apply(null, args);
}
};
/**
* Records a call to 'methodName' with arguments 'args'.
*
* @param {string} methodName The name of the called method.
* @param {!Array<?>} args The array of arguments.
* @private
*/
goog.labs.mock.MockManager_.prototype.recordCall_ =
function(methodName, args) {
var callRecord = new goog.labs.mock.MethodBinding_(methodName, args,
goog.nullFunction);
this.callRecords_.push(callRecord);
};
/**
* Verify invocation of a method with specific arguments.
*
* @param {string} methodName The name of the method.
* @param {...*} var_args The arguments passed.
* @protected
*/
goog.labs.mock.MockManager_.prototype.verifyInvocation =
function(methodName, var_args) {
var args = goog.array.slice(arguments, 1);
var binding = goog.array.find(this.callRecords_, function(binding) {
return binding.matches(methodName, args, true /* isVerification */);
});
if (!binding) {
throw new goog.labs.mock.VerificationError(
this.callRecords_, methodName, args);
}
};
/**
* Sets up mock for the given object (or class), stubbing out all the defined
* methods. By default, all stubs return {@code undefined}, though stubs can be
* later defined using {@code goog.labs.mock.when}.
*
* @param {!Object|!Function} objOrClass The object or class to set up the mock
* for. A class is a constructor function.
*
* @constructor
* @struct
* @extends {goog.labs.mock.MockManager_}
* @private
*/
goog.labs.mock.MockObjectManager_ = function(objOrClass) {
goog.labs.mock.MockObjectManager_.base(this, 'constructor');
/**
* Proxies the calls to establish the first step of the stub bindings (object
* and method name)
* @private
*/
this.objectStubBinder_ = {};
this.mockee = objOrClass;
/**
* The call verifier is used to verify the calls. It maps property names to
* the method that does call verification.
* @type {!Object<string, function(string, ...)>}
* @private
*/
this.objectCallVerifier_ = {};
var obj;
if (goog.isFunction(objOrClass)) {
// Create a temporary subclass with a no-op constructor so that we can
// create an instance and determine what methods it has.
/**
* @constructor
* @final
*/
var tempCtor = function() {};
goog.inherits(tempCtor, objOrClass);
obj = new tempCtor();
} else {
obj = objOrClass;
}
// Put the object being mocked in the prototype chain of the mock so that
// it has all the correct properties and instanceof works.
/**
* @constructor
* @final
*/
var mockedItemCtor = function() {};
mockedItemCtor.prototype = obj;
this.mockedItem = new mockedItemCtor();
var enumerableProperties = goog.object.getKeys(obj);
// The non enumerable properties are added due to the fact that IE8 does not
// enumerate any of the prototype Object functions even when overriden and
// mocking these is sometimes needed.
for (var i = 0; i < goog.labs.mock.PROTOTYPE_FIELDS_.length; i++) {
var prop = goog.labs.mock.PROTOTYPE_FIELDS_[i];
if (!goog.array.contains(enumerableProperties, prop)) {
enumerableProperties.push(prop);
}
}
// Adds the properties to the mock, creating a proxy stub for each method on
// the instance.
for (var i = 0; i < enumerableProperties.length; i++) {
var prop = enumerableProperties[i];
if (goog.isFunction(obj[prop])) {
this.mockedItem[prop] = goog.bind(this.executeStub, this, prop);
// The stub binder used to create bindings.
this.objectStubBinder_[prop] =
goog.bind(this.handleMockCall_, this, prop);
// The verifier verifies the calls.
this.objectCallVerifier_[prop] =
goog.bind(this.verifyInvocation, this, prop);
}
}
// The alias for stub binder exposed to the world.
this.mockedItem.$stubBinder = this.objectStubBinder_;
// The alias for verifier for the world.
this.mockedItem.$callVerifier = this.objectCallVerifier_;
};
goog.inherits(goog.labs.mock.MockObjectManager_,
goog.labs.mock.MockManager_);
/**
* Sets up the spying behavior for the given object.
*
* @param {!Object} obj The object to be spied on.
*
* @constructor
* @struct
* @extends {goog.labs.mock.MockObjectManager_}
* @private
*/
goog.labs.mock.MockSpyManager_ = function(obj) {
goog.labs.mock.MockSpyManager_.base(this, 'constructor', obj);
};
goog.inherits(goog.labs.mock.MockSpyManager_,
goog.labs.mock.MockObjectManager_);
/**
* Return a stub, if defined, for the method and arguments passed in. If we lack
* a stub, instead look for a call record that matches the method and arguments.
*
* @return {!Function} The stub or the invocation logger, if defined.
* @override
*/
goog.labs.mock.MockSpyManager_.prototype.getNextBinding =
function(methodName, args) {
var stub = goog.labs.mock.MockSpyManager_.base(
this, 'getNextBinding', methodName, args);
if (!stub) {
stub = goog.bind(this.mockee[methodName], this.mockee);
}
return stub;
};
/**
* Sets up mock for the given function, stubbing out. By default, all stubs
* return {@code undefined}, though stubs can be later defined using
* {@code goog.labs.mock.when}.
*
* @param {!Function} func The function to set up the mock for.
*
* @constructor
* @struct
* @extends {goog.labs.mock.MockManager_}
* @private
*/
goog.labs.mock.MockFunctionManager_ = function(func) {
goog.labs.mock.MockFunctionManager_.base(this, 'constructor');
this.func_ = func;
/**
* The stub binder used to create bindings.
* Sets the first argument of handleMockCall_ to the function name.
* @type {!Function}
* @private
*/
this.functionStubBinder_ = this.useMockedFunctionName_(this.handleMockCall_);
this.mockedItem = this.useMockedFunctionName_(this.executeStub);
this.mockedItem.$stubBinder = this.functionStubBinder_;
/**
* The call verifier is used to verify function invocations.
* Sets the first argument of verifyInvocation to the function name.
* @type {!Function}
*/
this.mockedItem.$callVerifier =
this.useMockedFunctionName_(this.verifyInvocation);
};
goog.inherits(goog.labs.mock.MockFunctionManager_,
goog.labs.mock.MockManager_);
/**
* Given a method, returns a new function that calls the first one setting
* the first argument to the mocked function name.
* This is used to dynamically override the stub binders and call verifiers.
* @private
* @param {Function} nextFunc The function to override.
* @return {!Function} The overloaded function.
*/
goog.labs.mock.MockFunctionManager_.prototype.useMockedFunctionName_ =
function(nextFunc) {
return goog.bind(function(var_args) {
var args = goog.array.slice(arguments, 0);
var name =
'#mockFor<' + goog.labs.mock.getFunctionName_(this.func_) + '>';
goog.array.insertAt(args, name, 0);
return nextFunc.apply(this, args);
}, this);
};
/**
* The stub binder is the object that helps define the stubs by binding
* method name to the stub method.
*
* @param {!goog.labs.mock.MockManager_}
* mockManager The mock manager.
* @param {?string} name The method name.
* @param {!Array<?>} args The other arguments to the method.
*
* @constructor
* @struct
* @private
*/
goog.labs.mock.StubBinder_ = function(mockManager, name, args) {
/**
* The mock manager instance.
* @type {!goog.labs.mock.MockManager_}
* @private
*/
this.mockManager_ = mockManager;
/**
* Holds the name of the method to be bound.
* @type {?string}
* @private
*/
this.name_ = name;
/**
* Holds the arguments for the method.
* @type {!Array<?>}
* @private
*/
this.args_ = args;
};
/**
* Defines the stub to be called for the method name and arguments bound
* earlier.
* TODO(user): Add support for the 'Answer' interface.
*
* @param {!Function} func The stub.
*/
goog.labs.mock.StubBinder_.prototype.then = function(func) {
this.mockManager_.addBinding(this.name_, this.args_, func);
};
/**
* Defines the stub to return a specific value for a method name and arguments.
*
* @param {*} value The value to return.
*/
goog.labs.mock.StubBinder_.prototype.thenReturn = function(value) {
this.mockManager_.addBinding(this.name_, this.args_,
goog.functions.constant(value));
};
/**
* Facilitates (and is the first step in) setting up stubs. Obtains an object
* on which, the method to be mocked is called to create a stub. Sample usage:
*
* var mockObj = goog.labs.mock.mock(objectBeingMocked);
* goog.labs.mock.when(mockObj).getFoo(3).thenReturn(4);
*
* @param {!Object} mockObject The mocked object.
* @return {!goog.labs.mock.StubBinder_} The property binder.
*/
goog.labs.mock.when = function(mockObject) {
goog.asserts.assert(mockObject.$stubBinder, 'Stub binder cannot be null!');
return mockObject.$stubBinder;
};
/**
* Represents a binding between a method name, args and a stub.
*
* @param {?string} methodName The name of the method being stubbed.
* @param {!Array<?>} args The arguments passed to the method.
* @param {!Function} stub The stub function to be called for the given method.
* @constructor
* @struct
* @private
*/
goog.labs.mock.MethodBinding_ = function(methodName, args, stub) {
/**
* The name of the method being stubbed.
* @type {?string}
* @private
*/
this.methodName_ = methodName;
/**
* The arguments for the method being stubbed.
* @type {!Array<?>}
* @private
*/
this.args_ = args;
/**
* The stub function.
* @type {!Function}
* @private
*/
this.stub_ = stub;
};
/**
* @return {!Function} The stub to be executed.
*/
goog.labs.mock.MethodBinding_.prototype.getStub = function() {
return this.stub_;
};
/**
* @override
* @return {string} A readable string representation of the binding
* as a method call.
*/
goog.labs.mock.MethodBinding_.prototype.toString = function() {
return goog.labs.mock.formatMethodCall_(this.methodName_ || '', this.args_);
};
/**
* @return {string} The method name for this binding.
*/
goog.labs.mock.MethodBinding_.prototype.getMethodName = function() {
return this.methodName_ || '';
};
/**
* Determines whether the given args match the stored args_. Used to determine
* which stub to invoke for a method.
*
* @param {string} methodName The name of the method being stubbed.
* @param {!Array<?>} args An array of arguments.
* @param {boolean} isVerification Whether this is a function verification call
* or not.
* @return {boolean} If it matches the stored arguments.
*/
goog.labs.mock.MethodBinding_.prototype.matches = function(
methodName, args, isVerification) {
var specs = isVerification ? args : this.args_;
var calls = isVerification ? this.args_ : args;
//TODO(user): More elaborate argument matching. Think about matching
// objects.
return this.methodName_ == methodName &&
goog.array.equals(calls, specs, function(arg, spec) {
// Duck-type to see if this is an object that implements the
// goog.labs.testing.Matcher interface.
if (goog.isFunction(spec.matches)) {
return spec.matches(arg);
} else {
return goog.array.defaultCompareEquality(spec, arg);
}
});
};
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - goog.labs.mock
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.mockTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,517 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.mockTest');
goog.setTestOnly('goog.labs.mockTest');
goog.require('goog.array');
goog.require('goog.labs.mock');
goog.require('goog.labs.mock.VerificationError');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.AnythingMatcher');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.GreaterThanMatcher');
goog.require('goog.string');
goog.require('goog.testing.jsunit');
var ParentClass = function() {};
ParentClass.prototype.method1 = function() {};
ParentClass.prototype.x = 1;
ParentClass.prototype.val = 0;
ParentClass.prototype.incrementVal = function() { this.val++; };
var ChildClass = function() {};
goog.inherits(ChildClass, ParentClass);
ChildClass.prototype.method2 = function() {};
ChildClass.prototype.y = 2;
function testParentClass() {
var parentMock = goog.labs.mock.mock(ParentClass);
assertNotUndefined(parentMock.method1);
assertUndefined(parentMock.method1());
assertUndefined(parentMock.method2);
assertNotUndefined(parentMock.x);
assertUndefined(parentMock.y);
assertTrue('Mock should be an instance of the mocked class.',
parentMock instanceof ParentClass);
}
function testChildClass() {
var childMock = goog.labs.mock.mock(ChildClass);
assertNotUndefined(childMock.method1);
assertUndefined(childMock.method1());
assertNotUndefined(childMock.method2);
assertUndefined(childMock.method2());
assertNotUndefined(childMock.x);
assertNotUndefined(childMock.y);
assertTrue('Mock should be an instance of the mocked class.',
childMock instanceof ChildClass);
}
function testParentClassInstance() {
var parentMock = goog.labs.mock.mock(new ParentClass());
assertNotUndefined(parentMock.method1);
assertUndefined(parentMock.method1());
assertUndefined(parentMock.method2);
assertNotUndefined(parentMock.x);
assertUndefined(parentMock.y);
assertTrue('Mock should be an instance of the mocked class.',
parentMock instanceof ParentClass);
}
function testChildClassInstance() {
var childMock = goog.labs.mock.mock(new ChildClass());
assertNotUndefined(childMock.method1);
assertUndefined(childMock.method1());
assertNotUndefined(childMock.method2);
assertUndefined(childMock.method2());
assertNotUndefined(childMock.x);
assertNotUndefined(childMock.y);
assertTrue('Mock should be an instance of the mocked class.',
childMock instanceof ParentClass);
}
function testNonEnumerableProperties() {
var mockObject = goog.labs.mock.mock({});
assertNotUndefined(mockObject.toString);
goog.labs.mock.when(mockObject).toString().then(function() {
return 'toString';
});
assertEquals('toString', mockObject.toString());
}
function testBasicStubbing() {
var obj = {
method1: function(i) {
return 2 * i;
},
method2: function(i, str) {
return str;
},
method3: function(x) {
return x;
}
};
var mockObj = goog.labs.mock.mock(obj);
goog.labs.mock.when(mockObj).method1(2).then(function(i) {return i;});
assertEquals(4, obj.method1(2));
assertEquals(2, mockObj.method1(2));
assertUndefined(mockObj.method1(4));
goog.labs.mock.when(mockObj).method2(1, 'hi').then(function(i) {return 'oh'});
assertEquals('hi', obj.method2(1, 'hi'));
assertEquals('oh', mockObj.method2(1, 'hi'));
assertUndefined(mockObj.method2(3, 'foo'));
goog.labs.mock.when(mockObj).method3(4).thenReturn(10);
assertEquals(4, obj.method3(4));
assertEquals(10, mockObj.method3(4));
goog.labs.mock.verify(mockObj).method3(4);
assertUndefined(mockObj.method3(5));
}
function testMockFunctions() {
function x(i) { return i; }
var mockedFunc = goog.labs.mock.mockFunction(x);
goog.labs.mock.when(mockedFunc)(100).thenReturn(10);
goog.labs.mock.when(mockedFunc)(50).thenReturn(25);
assertEquals(100, x(100));
assertEquals(10, mockedFunc(100));
assertEquals(25, mockedFunc(50));
}
function testStubbingConsecutiveCalls() {
var obj = {
method: function(i) {
return i * 42;
}
};
var mockObj = goog.labs.mock.mock(obj);
goog.labs.mock.when(mockObj).method(1).thenReturn(3);
goog.labs.mock.when(mockObj).method(1).thenReturn(4);
assertEquals(42, obj.method(1));
assertEquals(3, mockObj.method(1));
assertEquals(4, mockObj.method(1));
assertEquals(4, mockObj.method(1));
var x = function(i) { return i; };
var mockedFunc = goog.labs.mock.mockFunction(x);
goog.labs.mock.when(mockedFunc)(100).thenReturn(10);
goog.labs.mock.when(mockedFunc)(100).thenReturn(25);
assertEquals(100, x(100));
assertEquals(10, mockedFunc(100));
assertEquals(25, mockedFunc(100));
assertEquals(25, mockedFunc(100));
}
function testSpying() {
var obj = {
method1: function(i) {
return 2 * i;
},
method2: function(i) {
return 5 * i;
}
};
var spyObj = goog.labs.mock.spy(obj);
goog.labs.mock.when(spyObj).method1(2).thenReturn(5);
assertEquals(2, obj.method1(1));
assertEquals(5, spyObj.method1(2));
goog.labs.mock.verify(spyObj).method1(2);
assertEquals(2, spyObj.method1(1));
goog.labs.mock.verify(spyObj).method1(1);
assertEquals(20, spyObj.method2(4));
goog.labs.mock.verify(spyObj).method2(4);
}
function testSpyParentClassInstance() {
var parent = new ParentClass();
var parentMock = goog.labs.mock.spy(parent);
assertNotUndefined(parentMock.method1);
assertUndefined(parentMock.method1());
assertUndefined(parentMock.method2);
assertNotUndefined(parentMock.x);
assertUndefined(parentMock.y);
assertTrue('Mock should be an instance of the mocked class.',
parentMock instanceof ParentClass);
var incrementedOrigVal = parent.val + 1;
parentMock.incrementVal();
assertEquals('Changes in the spied object should reflect in the spy.',
incrementedOrigVal, parentMock.val);
}
function testSpyChildClassInstance() {
var child = new ChildClass();
var childMock = goog.labs.mock.spy(child);
assertNotUndefined(childMock.method1);
assertUndefined(childMock.method1());
assertNotUndefined(childMock.method2);
assertUndefined(childMock.method2());
assertNotUndefined(childMock.x);
assertNotUndefined(childMock.y);
assertTrue('Mock should be an instance of the mocked class.',
childMock instanceof ParentClass);
var incrementedOrigVal = child.val + 1;
childMock.incrementVal();
assertEquals('Changes in the spied object should reflect in the spy.',
incrementedOrigVal, childMock.val);
}
function testVerifyForObjects() {
var obj = {
method1: function(i) {
return 2 * i;
},
method2: function(i) {
return 5 * i;
}
};
var mockObj = goog.labs.mock.mock(obj);
goog.labs.mock.when(mockObj).method1(2).thenReturn(5);
assertEquals(5, mockObj.method1(2));
goog.labs.mock.verify(mockObj).method1(2);
var e = assertThrows(goog.bind(goog.labs.mock.verify(mockObj).method1, 2));
assertTrue(e instanceof goog.labs.mock.VerificationError);
}
function testVerifyForFunctions() {
var func = function(i) {
return i;
};
var mockFunc = goog.labs.mock.mockFunction(func);
goog.labs.mock.when(mockFunc)(2).thenReturn(55);
assertEquals(55, mockFunc(2));
goog.labs.mock.verify(mockFunc)(2);
goog.labs.mock.verify(mockFunc)(lessThan(3));
var e = assertThrows(goog.bind(goog.labs.mock.verify(mockFunc), 3));
assertTrue(e instanceof goog.labs.mock.VerificationError);
}
/**
* When a function invocation verification fails, it should show the failed
* expectation call, as well as the recorded calls to the same method.
*/
function testVerificationErrorMessages() {
var mock = goog.labs.mock.mock({
method: function(i) {
return i;
}
});
// Failure when there are no recorded calls.
var e = assertThrows(function() { goog.labs.mock.verify(mock).method(4); });
assertTrue(e instanceof goog.labs.mock.VerificationError);
var expected = '\nExpected: method(4)\n' +
'Recorded: No recorded calls';
assertEquals(expected, e.message);
// Failure when there are recorded calls with ints and functions
// as arguments.
var callback = function() {};
var callbackId = goog.labs.mock.getUid(callback);
mock.method(1);
mock.method(2);
mock.method(callback);
e = assertThrows(function() { goog.labs.mock.verify(mock).method(3); });
assertTrue(e instanceof goog.labs.mock.VerificationError);
expected = '\nExpected: method(3)\n' +
'Recorded: method(1),\n' +
' method(2),\n' +
' method(<function #anonymous' + callbackId + '>)';
assertEquals(expected, e.message);
// With mockFunctions
var mockCallback = goog.labs.mock.mockFunction(callback);
e = assertThrows(function() { goog.labs.mock.verify(mockCallback)(5);});
expected = '\nExpected: #mockFor<#anonymous' + callbackId + '>(5)\n' +
'Recorded: No recorded calls';
mockCallback(8);
goog.labs.mock.verify(mockCallback)(8);
assertEquals(expected, e.message);
// Objects with circular references should not fail.
var obj = {x: 1};
obj.y = obj;
mockCallback(obj);
e = assertThrows(function() { goog.labs.mock.verify(mockCallback)(5);});
assertTrue(e instanceof goog.labs.mock.VerificationError);
// Should respect string representation of different custom classes.
var myClass = function() {};
myClass.prototype.toString = function() { return '<superClass>'; };
var mockFunction = goog.labs.mock.mockFunction(function f() {});
mockFunction(new myClass());
e = assertThrows(function() { goog.labs.mock.verify(mockFunction)(5);});
expected = '\nExpected: #mockFor<f>(5)\n' +
'Recorded: #mockFor<f>(<superClass>)';
assertEquals(expected, e.message);
}
/**
* Asserts that the given string contains a list of others strings
* in the given order.
*/
function assertContainsInOrder(str, var_args) {
var expected = goog.array.splice(arguments, 1);
var indices = goog.array.map(expected, function(val) {
return str.indexOf(val);
});
for (var i = 0; i < expected.length; i++) {
var msg = 'Missing "' + expected[i] + '" from "' + str + '"';
assertTrue(msg, indices[i] != -1);
if (i > 0) {
msg = '"' + expected[i - 1] + '" should come before "' + expected[i] +
'" in "' + str + '"';
assertTrue(msg, indices[i] > indices[i - 1]);
}
}
}
function testMatchers() {
var obj = {
method1: function(i) {
return 2 * i;
},
method2: function(i) {
return 5 * i;
}
};
var mockObj = goog.labs.mock.mock(obj);
goog.labs.mock.when(mockObj).method1(greaterThan(4)).thenReturn(100);
goog.labs.mock.when(mockObj).method1(lessThan(4)).thenReturn(40);
assertEquals(100, mockObj.method1(5));
assertEquals(100, mockObj.method1(6));
assertEquals(40, mockObj.method1(2));
assertEquals(40, mockObj.method1(1));
assertUndefined(mockObj.method1(4));
}
function testMatcherVerify() {
var obj = {
method: function(i) {
return 2 * i;
}
};
// Using spy objects.
var spy = goog.labs.mock.spy(obj);
spy.method(6);
goog.labs.mock.verify(spy).method(greaterThan(4));
var e = assertThrows(
goog.bind(goog.labs.mock.verify(spy).method, lessThan(4)));
assertTrue(e instanceof goog.labs.mock.VerificationError);
// Using mocks
var mockObj = goog.labs.mock.mock(obj);
mockObj.method(8);
goog.labs.mock.verify(mockObj).method(greaterThan(7));
var e = assertThrows(
goog.bind(goog.labs.mock.verify(mockObj).method, lessThan(7)));
assertTrue(e instanceof goog.labs.mock.VerificationError);
}
function testMatcherVerifyCollision() {
var obj = {
method: function(i) {
return 2 * i;
}
};
var mockObj = goog.labs.mock.mock(obj);
goog.labs.mock.when(mockObj).method(5).thenReturn(100);
assertNotEquals(100, mockObj.method(greaterThan(2)));
}
function testMatcherVerifyCollisionBetweenMatchers() {
var obj = {
method: function(i) {
return 2 * i;
}
};
var mockObj = goog.labs.mock.mock(obj);
goog.labs.mock.when(mockObj).method(anything()).thenReturn(100);
var e = assertThrows(
goog.bind(goog.labs.mock.verify(mockObj).method, anything()));
assertTrue(e instanceof goog.labs.mock.VerificationError);
}
function testVerifyForUnmockedMethods() {
var Task = function() {};
Task.prototype.run = function() {};
var mockTask = goog.labs.mock.mock(Task);
mockTask.run();
goog.labs.mock.verify(mockTask).run();
}
function testFormatMethodCall() {
var formatMethodCall = goog.labs.mock.formatMethodCall_;
assertEquals('alert()', formatMethodCall('alert'));
assertEquals('sum(2, 4)', formatMethodCall('sum', [2, 4]));
assertEquals('sum("2", "4")', formatMethodCall('sum', ['2', '4']));
assertEquals('call(<function unicorn>)',
formatMethodCall('call', [function unicorn() {}]));
var arg = {x: 1, y: {hello: 'world'}};
assertEquals('call(' + goog.labs.mock.formatValue_(arg) + ')',
formatMethodCall('call', [arg]));
}
function testGetFunctionName() {
var f1 = function() {};
var f2 = function() {};
var named = function myName() {};
assert(goog.string.startsWith(
goog.labs.mock.getFunctionName_(f1), '#anonymous'));
assert(goog.string.startsWith(
goog.labs.mock.getFunctionName_(f2), '#anonymous'));
assertNotEquals(
goog.labs.mock.getFunctionName_(f1), goog.labs.mock.getFunctionName_(f2));
assertEquals('myName', goog.labs.mock.getFunctionName_(named));
}
function testFormatObject() {
var obj, obj2, obj3;
obj = {x: 1};
assertEquals(
'{"x":1 _id:' + goog.labs.mock.getUid(obj) + '}',
goog.labs.mock.formatValue_(obj)
);
assertEquals('{"x":1}', goog.labs.mock.formatValue_(obj, false /* id */));
obj = {x: 'hello'};
assertEquals(
'{"x":"hello" _id:' + goog.labs.mock.getUid(obj) + '}',
goog.labs.mock.formatValue_(obj)
);
assertEquals('{"x":"hello"}',
goog.labs.mock.formatValue_(obj, false /* id */));
obj3 = {};
obj2 = {y: obj3};
obj3.x = obj2;
assertEquals(
'{"x":{"y":<recursive/dupe obj_' + goog.labs.mock.getUid(obj3) + '> ' +
'_id:' + goog.labs.mock.getUid(obj2) + '} ' +
'_id:' + goog.labs.mock.getUid(obj3) + '}',
goog.labs.mock.formatValue_(obj3)
);
assertEquals('{"x":{"y":<recursive/dupe>}}',
goog.labs.mock.formatValue_(obj3, false /* id */)
);
obj = {x: function y() {} };
assertEquals('{"x":<function y> _id:' + goog.labs.mock.getUid(obj) + '}',
goog.labs.mock.formatValue_(obj));
assertEquals('{"x":<function y>}',
goog.labs.mock.formatValue_(obj, false /* id */));
}
function testGetUid() {
var obj1 = {};
var obj2 = {};
var func1 = function() {};
var func2 = function() {};
assertNotEquals(goog.labs.mock.getUid(obj1), goog.labs.mock.getUid(obj2));
assertNotEquals(goog.labs.mock.getUid(func1), goog.labs.mock.getUid(func2));
assertNotEquals(goog.labs.mock.getUid(obj1), goog.labs.mock.getUid(func2));
assertEquals(goog.labs.mock.getUid(obj1), goog.labs.mock.getUid(obj1));
assertEquals(goog.labs.mock.getUid(func1), goog.labs.mock.getUid(func1));
}
@@ -0,0 +1,94 @@
// Copyright 2012 The Closure Library 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 Simple image loader, used for preloading.
* @author nnaze@google.com (Nathan Naze)
*/
goog.provide('goog.labs.net.image');
goog.require('goog.Promise');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.net.EventType');
goog.require('goog.userAgent');
/**
* Loads a single image. Useful for preloading images.
*
* @param {string} uri URI of the image.
* @param {(!Image|function(): !Image)=} opt_image If present, instead of
* creating a new Image instance the function will use the passed Image
* instance or the result of calling the Image factory respectively. This
* can be used to control exactly how Image instances are created, for
* example if they should be created in a particular document element, or
* have fields that will trigger CORS image fetches.
* @return {!goog.Promise<!Image>} A Promise that will be resolved with the
* given image if the image successfully loads.
*/
goog.labs.net.image.load = function(uri, opt_image) {
return new goog.Promise(function(resolve, reject) {
var image;
if (!goog.isDef(opt_image)) {
image = new Image();
} else if (goog.isFunction(opt_image)) {
image = opt_image();
} else {
image = opt_image;
}
// IE's load event on images can be buggy. For older browsers, wait for
// readystatechange events and check if readyState is 'complete'.
// See:
// http://msdn.microsoft.com/en-us/library/ie/ms536957(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/ie/ms534359(v=vs.85).aspx
//
// Starting with IE11, start using standard 'load' events.
// See:
// http://msdn.microsoft.com/en-us/library/ie/dn467845(v=vs.85).aspx
var loadEvent = (goog.userAgent.IE && goog.userAgent.VERSION < 11) ?
goog.net.EventType.READY_STATE_CHANGE : goog.events.EventType.LOAD;
var handler = new goog.events.EventHandler();
handler.listen(
image,
[loadEvent, goog.net.EventType.ABORT, goog.net.EventType.ERROR],
function(e) {
// We only registered listeners for READY_STATE_CHANGE for IE.
// If readyState is now COMPLETE, the image has loaded.
// See related comment above.
if (e.type == goog.net.EventType.READY_STATE_CHANGE &&
image.readyState != goog.net.EventType.COMPLETE) {
return;
}
// At this point, we know whether the image load was successful
// and no longer care about image events.
goog.dispose(handler);
// Whether the image successfully loaded.
if (e.type == loadEvent) {
resolve(image);
} else {
reject(null);
}
});
// Initiate the image request.
image.src = uri;
});
};
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: nnaze@google.com (Nathan Naze)
-->
<head>
<title>Closure Unit Tests - goog.labs.net.image</title>
<script src="../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.imageTest');
</script>
</body>
</html>
@@ -0,0 +1,87 @@
// Copyright 2012 The Closure Library 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 Unit tests for goog.labs.net.Image.
*
* @author nnaze@google.com (Nathan Naze)
*/
/** @suppress {extraProvide} */
goog.provide('goog.labs.net.imageTest');
goog.require('goog.labs.net.image');
goog.require('goog.string');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.setTestOnly('goog.labs.net.ImageTest');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
function testValidImage() {
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url).then(function(value) {
assertEquals('IMG', value.tagName);
assertTrue(goog.string.endsWith(value.src, url));
asyncTestCase.continueTesting();
});
}
function testInvalidImage() {
var url = 'testdata/invalid.gif'; // This file does not exist.
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url).then(
fail /* opt_onResolved */,
function() {
asyncTestCase.continueTesting();
});
}
function testImageFactory() {
var returnedImage = new Image();
var factory = function() {
return returnedImage;
};
var countedFactory = goog.testing.recordFunction(factory);
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url, countedFactory).then(function(value) {
assertEquals(returnedImage, value);
assertEquals(1, countedFactory.getCallCount());
asyncTestCase.continueTesting();
});
}
function testExistingImage() {
var image = new Image();
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
goog.labs.net.image.load(url, image).then(function(value) {
assertEquals(image, value);
asyncTestCase.continueTesting();
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

@@ -0,0 +1,2 @@
while(1);
{"stat":"ok","count":12345}
@@ -0,0 +1 @@
Just some data.
@@ -0,0 +1,302 @@
// Copyright 2013 The Closure Library 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 The API spec for the WebChannel messaging library.
*
* Similar to HTML5 WebSocket and Closure BrowserChannel, WebChannel
* offers an abstraction for point-to-point socket-like communication between
* a browser client and a remote origin.
*
* WebChannels are created via <code>WebChannel</code>. Multiple WebChannels
* may be multiplexed over the same WebChannelTransport, which represents
* the underlying physical connectivity over standard wire protocols
* such as HTTP and SPDY.
*
* A WebChannels in turn represents a logical communication channel between
* the client and server end point. A WebChannel remains open for
* as long as the client or server end-point allows.
*
* Messages may be delivered in-order or out-of-order, reliably or unreliably
* over the same WebChannel. Message delivery guarantees of a WebChannel is
* to be specified by the application code; and the choice of the
* underlying wire protocols is completely transparent to the API users.
*
* Client-to-client messaging via WebRTC based transport may also be support
* via the same WebChannel API in future.
*
* Note that we have no immediate plan to move this API out of labs. While
* the implementation is production ready, the API is subject to change
* (addition):
* 1. Completely new W3C APIs for Web messaging may emerge in near future.
* 2. New programming models for cloud (on the server-side) may require
* new APIs to be defined.
* 3. WebRTC DataChannel alignment
* Lastly, we also want to white-list all internal use cases. As a general rule,
* we expect most applications to rely on stateless/RPC services.
*
*/
goog.provide('goog.net.WebChannel');
goog.require('goog.events');
goog.require('goog.events.Event');
/**
* A WebChannel represents a logical bi-directional channel over which the
* client communicates with a remote server that holds the other endpoint
* of the channel. A WebChannel is always created in the context of a shared
* {@link WebChannelTransport} instance. It is up to the underlying client-side
* and server-side implementations to decide how or when multiplexing is
* to be enabled.
*
* @interface
* @extends {EventTarget}
*/
goog.net.WebChannel = function() {};
/**
* Configuration spec for newly created WebChannel instances.
*
* WebChannels are configured in the context of the containing
* {@link WebChannelTransport}. The configuration parameters are specified
* when a new instance of WebChannel is created via {@link WebChannelTransport}.
*
* messageHeaders: custom headers to be added to every message sent to the
* server.
*
* messageUrlParams: custom url query parameters to be added to every message
* sent to the server.
*
* clientProtocolHeaderRequired: whether a special header should be added to
* each message so that the server can dispatch webchannel messages without
* knowing the URL path prefix. Defaults to false.
*
* concurrentRequestLimit: the maximum number of in-flight HTTP requests allowed
* when SPDY is enabled. Currently we only detect SPDY in Chrome.
* This parameter defaults to 10. When SPDY is not enabled, this parameter
* will have no effect.
*
* supportsCrossDomainXhr: setting this to true to allow the use of sub-domains
* (as configured by the server) to send XHRs with the CORS withCredentials
* bit set to true.
*
* testUrl: the test URL for detecting connectivity during the initial
* handshake. This parameter defaults to "/<channel_url>/test".
*
*
* @typedef {{
* messageHeaders: (!Object<string, string>|undefined),
* messageUrlParams: (!Object<string, string>|undefined),
* clientProtocolHeaderRequired: (boolean|undefined),
* concurrentRequestLimit: (number|undefined),
* supportsCrossDomainXhr: (boolean|undefined),
* testUrl: (string|undefined)
* }}
*/
goog.net.WebChannel.Options;
/**
* Types that are allowed as message data.
*
* @typedef {(ArrayBuffer|Blob|Object<string, string>|Array)}
*/
goog.net.WebChannel.MessageData;
/**
* Open the WebChannel against the URI specified in the constructor.
*/
goog.net.WebChannel.prototype.open = goog.abstractMethod;
/**
* Close the WebChannel.
*/
goog.net.WebChannel.prototype.close = goog.abstractMethod;
/**
* Sends a message to the server that maintains the other end point of
* the WebChannel.
*
* @param {!goog.net.WebChannel.MessageData} message The message to send.
*/
goog.net.WebChannel.prototype.send = goog.abstractMethod;
/**
* Common events fired by WebChannels.
* @enum {string}
*/
goog.net.WebChannel.EventType = {
/** Dispatched when the channel is opened. */
OPEN: goog.events.getUniqueId('open'),
/** Dispatched when the channel is closed. */
CLOSE: goog.events.getUniqueId('close'),
/** Dispatched when the channel is aborted due to errors. */
ERROR: goog.events.getUniqueId('error'),
/** Dispatched when the channel has received a new message. */
MESSAGE: goog.events.getUniqueId('message')
};
/**
* The event interface for the MESSAGE event.
*
* @constructor
* @extends {goog.events.Event}
*/
goog.net.WebChannel.MessageEvent = function() {
goog.net.WebChannel.MessageEvent.base(
this, 'constructor', goog.net.WebChannel.EventType.MESSAGE);
};
goog.inherits(goog.net.WebChannel.MessageEvent, goog.events.Event);
/**
* The content of the message received from the server.
*
* @type {!goog.net.WebChannel.MessageData}
*/
goog.net.WebChannel.MessageEvent.prototype.data;
/**
* WebChannel level error conditions.
* @enum {number}
*/
goog.net.WebChannel.ErrorStatus = {
/** No error has occurred. */
OK: 0,
/** Communication to the server has failed. */
NETWORK_ERROR: 1,
/** The server fails to accept the WebChannel. */
SERVER_ERROR: 2
};
/**
* The event interface for the ERROR event.
*
* @constructor
* @extends {goog.events.Event}
*/
goog.net.WebChannel.ErrorEvent = function() {
goog.net.WebChannel.ErrorEvent.base(
this, 'constructor', goog.net.WebChannel.EventType.ERROR);
};
goog.inherits(goog.net.WebChannel.ErrorEvent, goog.events.Event);
/**
* The error status.
*
* @type {!goog.net.WebChannel.ErrorStatus}
*/
goog.net.WebChannel.ErrorEvent.prototype.status;
/**
* @return {!goog.net.WebChannel.RuntimeProperties} The runtime properties
* of the WebChannel instance.
*/
goog.net.WebChannel.prototype.getRuntimeProperties = goog.abstractMethod;
/**
* The readonly runtime properties of the WebChannel instance.
*
* This class is defined for debugging and monitoring purposes, and for
* optimization functions that the application may choose to manage by itself.
*
* @interface
*/
goog.net.WebChannel.RuntimeProperties = function() {};
/**
* @return {number} The effective limit for the number of concurrent HTTP
* requests that are allowed to be made for sending messages from the client
* to the server. When SPDY is not enabled, this limit will be one.
*/
goog.net.WebChannel.RuntimeProperties.prototype.getConcurrentRequestLimit =
goog.abstractMethod;
/**
* For applications that need support multiple channels (e.g. from
* different tabs) to the same origin, use this method to decide if SPDY is
* enabled and therefore it is safe to open multiple channels.
*
* If SPDY is disabled, the application may choose to limit the number of active
* channels to one or use other means such as sub-domains to work around
* the browser connection limit.
*
* @return {boolean} Whether SPDY is enabled for the origin against which
* the channel is created.
*/
goog.net.WebChannel.RuntimeProperties.prototype.isSpdyEnabled =
goog.abstractMethod;
/**
* This method may be used by the application to stop ack of received messages
* as a means of enabling or disabling flow-control on the server-side.
*
* @param {boolean} enabled If true, enable flow-control behavior on the
* server side. Setting it to false will cancel ay previous enabling action.
*/
goog.net.WebChannel.RuntimeProperties.prototype.setServerFlowControl =
goog.abstractMethod;
/**
* This method may be used by the application to throttle the rate of outgoing
* messages, as a means of sender initiated flow-control.
*
* @return {number} The total number of messages that have not received
* ack from the server and therefore remain in the buffer.
*/
goog.net.WebChannel.RuntimeProperties.prototype.getNonAckedMessageCount =
goog.abstractMethod;
/**
* A special header to indicate to the server what messaging protocol
* each HTTP message is speaking.
*
* @type {string}
*/
goog.net.WebChannel.X_CLIENT_PROTOCOL = 'X-Client-Protocol';
/**
* The value for x-client-protocol when the messaging protocol is WebChannel.
*
* @type {string}
*/
goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL = 'webchannel';
@@ -0,0 +1,519 @@
// Copyright 2006 The Closure Library 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 Base TestChannel implementation.
*
*/
goog.provide('goog.labs.net.webChannel.BaseTestChannel');
goog.require('goog.labs.net.webChannel.Channel');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.labs.net.webChannel.requestStats.Stat');
/**
* A TestChannel is used during the first part of channel negotiation
* with the server to create the channel. It helps us determine whether we're
* behind a buffering proxy.
*
* @constructor
* @struct
* @param {!goog.labs.net.webChannel.Channel} channel The channel
* that owns this test channel.
* @param {!goog.labs.net.webChannel.WebChannelDebug} channelDebug A
* WebChannelDebug instance to use for logging.
* @implements {goog.labs.net.webChannel.Channel}
*/
goog.labs.net.webChannel.BaseTestChannel = function(channel, channelDebug) {
/**
* The channel that owns this test channel
* @private {!goog.labs.net.webChannel.Channel}
*/
this.channel_ = channel;
/**
* The channel debug to use for logging
* @private {!goog.labs.net.webChannel.WebChannelDebug}
*/
this.channelDebug_ = channelDebug;
/**
* Extra HTTP headers to add to all the requests sent to the server.
* @private {Object}
*/
this.extraHeaders_ = null;
/**
* The test request.
* @private {goog.labs.net.webChannel.ChannelRequest}
*/
this.request_ = null;
/**
* Whether we have received the first result as an intermediate result. This
* helps us determine whether we're behind a buffering proxy.
* @private {boolean}
*/
this.receivedIntermediateResult_ = false;
/**
* The time when the test request was started. We use timing in IE as
* a heuristic for whether we're behind a buffering proxy.
* @private {?number}
*/
this.startTime_ = null;
/**
* The time for of the first result part. We use timing in IE as a
* heuristic for whether we're behind a buffering proxy.
* @private {?number}
*/
this.firstTime_ = null;
/**
* The time for of the last result part. We use timing in IE as a
* heuristic for whether we're behind a buffering proxy.
* @private {?number}
*/
this.lastTime_ = null;
/**
* The relative path for test requests.
* @private {?string}
*/
this.path_ = null;
/**
* The last status code received.
* @private {number}
*/
this.lastStatusCode_ = -1;
/**
* A subdomain prefix for using a subdomain in IE for the backchannel
* requests.
* @private {?string}
*/
this.hostPrefix_ = null;
/**
* The effective client protocol as indicated by the initial handshake
* response via the x-client-wire-protocol header.
*
* @private {?string}
*/
this.clientProtocol_ = null;
};
goog.scope(function() {
var BaseTestChannel = goog.labs.net.webChannel.BaseTestChannel;
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
var requestStats = goog.labs.net.webChannel.requestStats;
var Channel = goog.labs.net.webChannel.Channel;
/**
* Enum type for the test channel state machine
* @enum {number}
* @private
*/
BaseTestChannel.State_ = {
/**
* The state for the TestChannel state machine where we making the
* initial call to get the server configured parameters.
*/
INIT: 0,
/**
* The state for the TestChannel state machine where we're checking to
* se if we're behind a buffering proxy.
*/
CONNECTION_TESTING: 1
};
/**
* The state of the state machine for this object.
*
* @private {?BaseTestChannel.State_}
*/
BaseTestChannel.prototype.state_ = null;
/**
* Time between chunks in the test connection that indicates that we
* are not behind a buffering proxy. This value should be less than or
* equals to the time between chunks sent from the server.
* @type {number}
* @private
*/
BaseTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_ = 500;
/**
* Sets extra HTTP headers to add to all the requests sent to the server.
*
* @param {Object} extraHeaders The HTTP headers.
*/
BaseTestChannel.prototype.setExtraHeaders = function(extraHeaders) {
this.extraHeaders_ = extraHeaders;
};
/**
* Starts the test channel. This initiates connections to the server.
*
* @param {string} path The relative uri for the test connection.
*/
BaseTestChannel.prototype.connect = function(path) {
this.path_ = path;
var sendDataUri = this.channel_.getForwardChannelUri(this.path_);
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_ONE_START);
this.startTime_ = goog.now();
// If the channel already has the result of the handshake, then skip it.
var handshakeResult = this.channel_.getConnectionState().handshakeResult;
if (goog.isDefAndNotNull(handshakeResult)) {
this.hostPrefix_ = this.channel_.correctHostPrefix(handshakeResult[0]);
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.checkBufferingProxy_();
return;
}
// the first request returns server specific parameters
sendDataUri.setParameterValues('MODE', 'init');
this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
this.request_.xmlHttpGet(sendDataUri, false /* decodeChunks */,
null /* hostPrefix */, true /* opt_noClose */);
this.state_ = BaseTestChannel.State_.INIT;
};
/**
* Begins the second stage of the test channel where we test to see if we're
* behind a buffering proxy. The server sends back a multi-chunked response
* with the first chunk containing the content '1' and then two seconds later
* sending the second chunk containing the content '2'. Depending on how we
* receive the content, we can tell if we're behind a buffering proxy.
* @private
*/
BaseTestChannel.prototype.checkBufferingProxy_ = function() {
this.channelDebug_.debug('TestConnection: starting stage 2');
// If the test result is already available, skip its execution.
var bufferingProxyResult =
this.channel_.getConnectionState().bufferingProxyResult;
if (goog.isDefAndNotNull(bufferingProxyResult)) {
this.channelDebug_.debug(
'TestConnection: skipping stage 2, precomputed result is ' +
bufferingProxyResult ? 'Buffered' : 'Unbuffered');
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
if (bufferingProxyResult) { // Buffered/Proxy connection
requestStats.notifyStatEvent(requestStats.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
} else { // Unbuffered/NoProxy connection
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
return; // Skip the test
}
this.request_ = ChannelRequest.createChannelRequest(this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
var recvDataUri = this.channel_.getBackChannelUri(this.hostPrefix_,
/** @type {string} */ (this.path_));
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
if (!ChannelRequest.supportsXhrStreaming()) {
recvDataUri.setParameterValues('TYPE', 'html');
this.request_.tridentGet(recvDataUri, Boolean(this.hostPrefix_));
} else {
recvDataUri.setParameterValues('TYPE', 'xmlhttp');
this.request_.xmlHttpGet(recvDataUri, false /** decodeChunks */,
this.hostPrefix_, false /** opt_noClose */);
}
};
/**
* @override
*/
BaseTestChannel.prototype.createXhrIo = function(hostPrefix) {
return this.channel_.createXhrIo(hostPrefix);
};
/**
* Aborts the test channel.
*/
BaseTestChannel.prototype.abort = function() {
if (this.request_) {
this.request_.cancel();
this.request_ = null;
}
this.lastStatusCode_ = -1;
};
/**
* Returns whether the test channel is closed. The ChannelRequest object expects
* this method to be implemented on its handler.
*
* @return {boolean} Whether the channel is closed.
* @override
*/
BaseTestChannel.prototype.isClosed = function() {
return false;
};
/**
* Callback from ChannelRequest for when new data is received
*
* @param {ChannelRequest} req The request object.
* @param {string} responseText The text of the response.
* @override
*/
BaseTestChannel.prototype.onRequestData = function(req, responseText) {
this.lastStatusCode_ = req.getLastStatusCode();
if (this.state_ == BaseTestChannel.State_.INIT) {
this.channelDebug_.debug('TestConnection: Got data for stage 1');
if (!responseText) {
this.channelDebug_.debug('TestConnection: Null responseText');
// The server should always send text; something is wrong here
this.channel_.testConnectionFailure(this, ChannelRequest.Error.BAD_DATA);
return;
}
/** @preserveTry */
try {
var respArray = this.channel_.getWireCodec().decodeMessage(responseText);
} catch (e) {
this.channelDebug_.dumpException(e);
this.channel_.testConnectionFailure(this, ChannelRequest.Error.BAD_DATA);
return;
}
this.hostPrefix_ = this.channel_.correctHostPrefix(respArray[0]);
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
if (this.receivedIntermediateResult_) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_DATA_TWO);
this.lastTime_ = goog.now();
} else {
// '11111' is used instead of '1' to prevent a small amount of buffering
// by Safari.
if (responseText == '11111') {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_DATA_ONE);
this.receivedIntermediateResult_ = true;
this.firstTime_ = goog.now();
if (this.checkForEarlyNonBuffered_()) {
// If early chunk detection is on, and we passed the tests,
// assume HTTP_OK, cancel the test and turn on noproxy mode.
this.lastStatusCode_ = 200;
this.request_.cancel();
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
} else {
requestStats.notifyStatEvent(
requestStats.Stat.TEST_STAGE_TWO_DATA_BOTH);
this.firstTime_ = this.lastTime_ = goog.now();
this.receivedIntermediateResult_ = false;
}
}
}
};
/**
* Callback from ChannelRequest that indicates a request has completed.
*
* @param {!ChannelRequest} req The request object.
* @override
*/
BaseTestChannel.prototype.onRequestComplete = function(req) {
this.lastStatusCode_ = this.request_.getLastStatusCode();
if (!this.request_.getSuccess()) {
this.channelDebug_.debug(
'TestConnection: request failed, in state ' + this.state_);
if (this.state_ == BaseTestChannel.State_.INIT) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_ONE_FAILED);
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_FAILED);
}
this.channel_.testConnectionFailure(this,
/** @type {ChannelRequest.Error} */
(this.request_.getLastError()));
return;
}
if (this.state_ == BaseTestChannel.State_.INIT) {
this.recordClientProtocol_(req);
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.channelDebug_.debug(
'TestConnection: request complete for initial check');
this.checkBufferingProxy_();
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
this.channelDebug_.debug('TestConnection: request complete for stage 2');
var goodConn = false;
if (!ChannelRequest.supportsXhrStreaming()) {
// we always get Trident responses in separate calls to
// onRequestData, so we have to check the time they came
var ms = this.lastTime_ - this.firstTime_;
if (ms < 200) {
// TODO: need to empirically verify that this number is OK
// for slow computers
goodConn = false;
} else {
goodConn = true;
}
} else {
goodConn = this.receivedIntermediateResult_;
}
if (goodConn) {
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
requestStats.notifyStatEvent(requestStats.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
} else {
this.channelDebug_.debug(
'Test connection failed; not using streaming');
requestStats.notifyStatEvent(requestStats.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
}
}
};
/**
* Record the client protocol header from the initial handshake response.
*
* @param {!ChannelRequest} req The request object.
* @private
*/
BaseTestChannel.prototype.recordClientProtocol_ = function(req) {
var xmlHttp = req.getXhr();
if (xmlHttp) {
var protocolHeader = xmlHttp.getResponseHeader('x-client-wire-protocol');
this.clientProtocol_ = protocolHeader ? protocolHeader : null;
}
};
/**
* @return {?string} The client protocol as recorded with the init handshake
* request.
*/
BaseTestChannel.prototype.getClientProtocol = function() {
return this.clientProtocol_;
};
/**
* Returns the last status code received for a request.
* @return {number} The last status code received for a request.
*/
BaseTestChannel.prototype.getLastStatusCode = function() {
return this.lastStatusCode_;
};
/**
* @return {boolean} Whether we should be using secondary domains when the
* server instructs us to do so.
* @override
*/
BaseTestChannel.prototype.shouldUseSecondaryDomains = function() {
return this.channel_.shouldUseSecondaryDomains();
};
/**
* @override
*/
BaseTestChannel.prototype.isActive = function() {
return this.channel_.isActive();
};
/**
* @return {boolean} True if test stage 2 detected a non-buffered
* channel early and early no buffering detection is enabled.
* @private
*/
BaseTestChannel.prototype.checkForEarlyNonBuffered_ = function() {
var ms = this.firstTime_ - this.startTime_;
// we always get Trident responses in separate calls to
// onRequestData, so we have to check the time that the first came in
// and verify that the data arrived before the second portion could
// have been sent. For all other browser's we skip the timing test.
return ChannelRequest.supportsXhrStreaming() ||
ms < BaseTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_;
};
/**
* @override
*/
BaseTestChannel.prototype.getForwardChannelUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getBackChannelUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.correctHostPrefix = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.createDataUri = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFinished = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getConnectionState = goog.abstractMethod;
}); // goog.scope
@@ -0,0 +1,186 @@
// Copyright 2013 The Closure Library 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 A shared interface for WebChannelBase and BaseTestChannel.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.Channel');
/**
* Shared interface between Channel and TestChannel to support callbacks
* between WebChannelBase and BaseTestChannel and between Channel and
* ChannelRequest.
*
* @interface
*/
goog.labs.net.webChannel.Channel = function() {};
goog.scope(function() {
var Channel = goog.labs.net.webChannel.Channel;
/**
* Determines whether to use a secondary domain when the server gives us
* a host prefix. This allows us to work around browser per-domain
* connection limits.
*
* Currently, we use secondary domains when using Trident's ActiveXObject,
* because it supports cross-domain requests out of the box. Note that in IE10
* we no longer use ActiveX since it's not supported in Metro mode and IE10
* supports XHR streaming.
*
* If you need to use secondary domains on other browsers and IE10,
* you have two choices:
* 1) If you only care about browsers that support CORS
* (https://developer.mozilla.org/en-US/docs/HTTP_access_control), you
* can use {@link #setSupportsCrossDomainXhrs} and set the appropriate
* CORS response headers on the server.
* 2) Or, override this method in a subclass, and make sure that those
* browsers use some messaging mechanism that works cross-domain (e.g
* iframes and window.postMessage).
*
* @return {boolean} Whether to use secondary domains.
* @see http://code.google.com/p/closure-library/issues/detail?id=339
*/
Channel.prototype.shouldUseSecondaryDomains = goog.abstractMethod;
/**
* Called when creating an XhrIo object. Override in a subclass if
* you need to customize the behavior, for example to enable the creation of
* XHR's capable of calling a secondary domain. Will also allow calling
* a secondary domain if withCredentials (CORS) is enabled.
* @param {?string} hostPrefix The host prefix, if we need an XhrIo object
* capable of calling a secondary domain.
* @return {!goog.net.XhrIo} A new XhrIo object.
*/
Channel.prototype.createXhrIo = goog.abstractMethod;
/**
* Callback from ChannelRequest that indicates a request has completed.
* @param {!goog.labs.net.webChannel.ChannelRequest} request
* The request object.
*/
Channel.prototype.onRequestComplete = goog.abstractMethod;
/**
* Returns whether the channel is closed
* @return {boolean} true if the channel is closed.
*/
Channel.prototype.isClosed = goog.abstractMethod;
/**
* Callback from ChannelRequest for when new data is received
* @param {goog.labs.net.webChannel.ChannelRequest} request
* The request object.
* @param {string} responseText The text of the response.
*/
Channel.prototype.onRequestData = goog.abstractMethod;
/**
* Gets whether this channel is currently active. This is used to determine the
* length of time to wait before retrying. This call delegates to the handler.
* @return {boolean} Whether the channel is currently active.
*/
Channel.prototype.isActive = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Gets the Uri used for the connection that sends data to the server.
* @param {string} path The path on the host.
* @return {goog.Uri} The forward channel URI.
*/
Channel.prototype.getForwardChannelUri = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Gets the Uri used for the connection that receives data from the server.
* @param {?string} hostPrefix The host prefix.
* @param {string} path The path on the host.
* @return {goog.Uri} The back channel URI.
*/
Channel.prototype.getBackChannelUri = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Allows the handler to override a host prefix provided by the server. Will
* be called whenever the channel has received such a prefix and is considering
* its use.
* @param {?string} serverHostPrefix The host prefix provided by the server.
* @return {?string} The host prefix the client should use.
*/
Channel.prototype.correctHostPrefix = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Creates a data Uri applying logic for secondary hostprefix, port
* overrides, and versioning.
* @param {?string} hostPrefix The host prefix.
* @param {string} path The path on the host (may be absolute or relative).
* @param {number=} opt_overridePort Optional override port.
* @return {goog.Uri} The data URI.
*/
Channel.prototype.createDataUri = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Callback from TestChannel for when the channel is finished.
* @param {goog.labs.net.webChannel.BaseTestChannel} testChannel
* The TestChannel.
* @param {boolean} useChunked Whether we can chunk responses.
*/
Channel.prototype.testConnectionFinished = goog.abstractMethod;
/**
* Not needed for testchannel.
*
* Callback from TestChannel for when the channel has an error.
* @param {goog.labs.net.webChannel.BaseTestChannel} testChannel
* The TestChannel.
* @param {goog.labs.net.webChannel.ChannelRequest.Error} errorCode
* The error code of the failure.
*/
Channel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* Not needed for testchannel.
* Gets the result of previous connectivity tests.
*
* @return {!goog.labs.net.webChannel.ConnectionState} The connectivity state.
*/
Channel.prototype.getConnectionState = goog.abstractMethod;
}); // goog.scope
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.ChannelRequest</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.channelRequestTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,310 @@
// Copyright 2013 The Closure Library 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 Unit tests for goog.labs.net.webChannel.ChannelRequest.
*
*/
goog.provide('goog.labs.net.webChannel.channelRequestTest');
goog.require('goog.Uri');
goog.require('goog.functions');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.WebChannelDebug');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.labs.net.webChannel.requestStats.ServerReachability');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIo');
goog.require('goog.testing.recordFunction');
goog.setTestOnly('goog.labs.net.webChannel.channelRequestTest');
var channelRequest;
var mockChannel;
var mockClock;
var stubs;
var xhrIo;
var reachabilityEvents;
/**
* Time to wait for a network request to time out, before aborting.
*/
var WATCHDOG_TIME = 2000;
/**
* Time to throttle readystatechange events.
*/
var THROTTLE_TIME = 500;
/**
* A really long time - used to make sure no more timeouts will fire.
*/
var ALL_DAY_MS = 1000 * 60 * 60 * 24;
function setUp() {
mockClock = new goog.testing.MockClock();
mockClock.install();
reachabilityEvents = {};
stubs = new goog.testing.PropertyReplacer();
// Mock out the stat notification code.
var notifyServerReachabilityEvent = function(reachabilityType) {
if (!reachabilityEvents[reachabilityType]) {
reachabilityEvents[reachabilityType] = 0;
}
reachabilityEvents[reachabilityType]++;
};
stubs.set(goog.labs.net.webChannel.requestStats,
'notifyServerReachabilityEvent', notifyServerReachabilityEvent);
}
function tearDown() {
stubs.reset();
mockClock.uninstall();
}
/**
* Constructs a duck-type WebChannelBase that tracks the completed requests.
* @constructor
* @struct
* @final
*/
function MockWebChannelBase() {
this.isClosed = function() {
return false;
};
this.isActive = function() {
return true;
};
this.shouldUseSecondaryDomains = function() {
return false;
};
this.completedRequests = [];
this.onRequestComplete = function(request) {
this.completedRequests.push(request);
};
this.onRequestData = function(request, data) {};
}
/**
* Creates a real ChannelRequest object, with some modifications for
* testability:
* <ul>
* <li>The channel is a mock channel.
* <li>The new watchdogTimeoutCallCount property tracks onWatchDogTimeout_()
* calls.
* <li>The timeout is set to WATCHDOG_TIME.
* </ul>
*/
function createChannelRequest() {
xhrIo = new goog.testing.net.XhrIo();
xhrIo.abort = xhrIo.abort || function() {
this.active_ = false;
};
// Install mock channel and no-op debug logger.
mockChannel = new MockWebChannelBase();
channelRequest = new goog.labs.net.webChannel.ChannelRequest(
mockChannel,
new goog.labs.net.webChannel.WebChannelDebug());
// Install test XhrIo.
mockChannel.createXhrIo = function() {
return xhrIo;
};
// Install watchdogTimeoutCallCount.
channelRequest.watchdogTimeoutCallCount = 0;
channelRequest.originalOnWatchDogTimeout = channelRequest.onWatchDogTimeout_;
channelRequest.onWatchDogTimeout_ = function() {
channelRequest.watchdogTimeoutCallCount++;
return channelRequest.originalOnWatchDogTimeout();
};
channelRequest.setTimeout(WATCHDOG_TIME);
}
/**
* Run through the lifecycle of a long lived request, checking that the right
* network events are reported.
*/
function testNetworkEvents() {
createChannelRequest();
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
checkReachabilityEvents(1, 0, 0, 0);
if (goog.labs.net.webChannel.ChannelRequest.supportsXhrStreaming()) {
xhrIo.simulatePartialResponse('17\nI am a BC Message');
checkReachabilityEvents(1, 0, 0, 1);
xhrIo.simulatePartialResponse('23\nI am another BC Message');
checkReachabilityEvents(1, 0, 0, 2);
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 2);
} else {
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 0);
}
}
/**
* Test throttling of readystatechange events.
*/
function testNetworkEvents_throttleReadyStateChange() {
createChannelRequest();
channelRequest.setReadyStateChangeThrottle(THROTTLE_TIME);
var recordedHandler =
goog.testing.recordFunction(channelRequest.xmlHttpHandler_);
stubs.set(channelRequest, 'xmlHttpHandler_', recordedHandler);
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
assertEquals(1, recordedHandler.getCallCount());
checkReachabilityEvents(1, 0, 0, 0);
if (goog.labs.net.webChannel.ChannelRequest.supportsXhrStreaming()) {
xhrIo.simulatePartialResponse('17\nI am a BC Message');
checkReachabilityEvents(1, 0, 0, 1);
assertEquals(3, recordedHandler.getCallCount());
// Second event should be throttled
xhrIo.simulatePartialResponse('23\nI am another BC Message');
assertEquals(3, recordedHandler.getCallCount());
xhrIo.simulatePartialResponse('27\nI am yet another BC Message');
assertEquals(3, recordedHandler.getCallCount());
mockClock.tick(THROTTLE_TIME);
checkReachabilityEvents(1, 0, 0, 3);
// Only one more call because of throttling.
assertEquals(4, recordedHandler.getCallCount());
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 3);
assertEquals(5, recordedHandler.getCallCount());
} else {
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 0);
}
}
/**
* Make sure that the request "completes" with an error when the timeout
* expires.
*/
function testRequestTimeout() {
createChannelRequest();
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
assertEquals(0, channelRequest.watchdogTimeoutCallCount);
assertEquals(0, channelRequest.channel_.completedRequests.length);
// Watchdog timeout.
mockClock.tick(WATCHDOG_TIME);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
assertFalse(channelRequest.getSuccess());
// Make sure no more timers are firing.
mockClock.tick(ALL_DAY_MS);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
checkReachabilityEvents(1, 0, 1, 0);
}
function testRequestTimeoutWithUnexpectedException() {
createChannelRequest();
channelRequest.channel_.createXhrIo = goog.functions.error('Weird error');
try {
channelRequest.xmlHttpGet(new goog.Uri('some_uri'), true, null);
fail('Expected error');
} catch (e) {
assertEquals('Weird error', e.message);
}
assertEquals(0, channelRequest.watchdogTimeoutCallCount);
assertEquals(0, channelRequest.channel_.completedRequests.length);
// Watchdog timeout.
mockClock.tick(WATCHDOG_TIME);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
assertFalse(channelRequest.getSuccess());
// Make sure no more timers are firing.
mockClock.tick(ALL_DAY_MS);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
checkReachabilityEvents(0, 0, 1, 0);
}
function testActiveXBlocked() {
createChannelRequest();
stubs.set(goog.global, 'ActiveXObject',
goog.functions.error('Active X blocked'));
channelRequest.tridentGet(new goog.Uri('some_uri'), false);
assertFalse(channelRequest.getSuccess());
assertEquals(
goog.labs.net.webChannel.ChannelRequest.Error.ACTIVE_X_BLOCKED,
channelRequest.getLastError());
checkReachabilityEvents(0, 0, 0, 0);
}
function checkReachabilityEvents(reqMade, reqSucceeded, reqFail, backChannel) {
var Reachability =
goog.labs.net.webChannel.requestStats.ServerReachability;
assertEquals(reqMade,
reachabilityEvents[Reachability.REQUEST_MADE] || 0);
assertEquals(reqSucceeded,
reachabilityEvents[Reachability.REQUEST_SUCCEEDED] || 0);
assertEquals(reqFail,
reachabilityEvents[Reachability.REQUEST_FAILED] || 0);
assertEquals(backChannel,
reachabilityEvents[Reachability.BACK_CHANNEL_ACTIVITY] || 0);
}
function testDuplicatedRandomParams() {
createChannelRequest();
channelRequest.xmlHttpGet(new goog.Uri('some_uri'), true, null, true,
true /* opt_duplicateRandom */);
var z = xhrIo.getLastUri().getParameterValue('zx');
var z1 = xhrIo.getLastUri().getParameterValue('zx1');
assertTrue(goog.isDefAndNotNull(z));
assertTrue(goog.isDefAndNotNull(z1));
assertEquals(z1, z);
}
@@ -0,0 +1,50 @@
// Copyright 2013 The Closure Library 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 This class manages the network connectivity state.
*
* Some of the connectivity state may be exposed to the client code in future,
* e.g. the initial handshake state, in order to save one RTT when a channel
* has to be reestablished. TODO(user).
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.ConnectionState');
/**
* The connectivity state of the channel.
*
* @constructor
* @struct
*/
goog.labs.net.webChannel.ConnectionState = function() {
/**
* Handshake result.
* @type {Array<string>}
*/
this.handshakeResult = null;
/**
* The result of checking if there is a buffering proxy in the network.
* True means the connection is buffered, False means unbuffered,
* null means that the result is not available.
* @type {?boolean}
*/
this.bufferingProxyResult = null;
};
@@ -0,0 +1,278 @@
// Copyright 2013 The Closure Library 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 A pool of forward channel requests to enable real-time
* messaging from the client to server.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.ForwardChannelRequestPool');
goog.require('goog.array');
goog.require('goog.string');
goog.require('goog.structs.Set');
goog.scope(function() {
// type checking only (no require)
var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
/**
* This class represents the state of all forward channel requests.
*
* @param {number=} opt_maxPoolSize The maximum pool size.
*
* @constructor
* @final
*/
goog.labs.net.webChannel.ForwardChannelRequestPool = function(opt_maxPoolSize) {
/**
* THe max pool size as configured.
*
* @private {number}
*/
this.maxPoolSizeConfigured_ = opt_maxPoolSize ||
goog.labs.net.webChannel.ForwardChannelRequestPool.MAX_POOL_SIZE_;
/**
* The current size limit of the request pool. This limit is meant to be
* read-only after the channel is fully opened.
*
* If SPDY is enabled, set it to the max pool size, which is also
* configurable.
*
* @private {number}
*/
this.maxSize_ = ForwardChannelRequestPool.isSpdyEnabled_() ?
this.maxPoolSizeConfigured_ : 1;
/**
* The container for all the pending request objects.
*
* @private {goog.structs.Set<ChannelRequest>}
*/
this.requestPool_ = null;
if (this.maxSize_ > 1) {
this.requestPool_ = new goog.structs.Set();
}
/**
* The single request object when the pool size is limited to one.
*
* @private {ChannelRequest}
*/
this.request_ = null;
};
var ForwardChannelRequestPool =
goog.labs.net.webChannel.ForwardChannelRequestPool;
/**
* The default size limit of the request pool.
*
* @private {number}
*/
ForwardChannelRequestPool.MAX_POOL_SIZE_ = 10;
/**
* @return {boolean} True if SPDY is enabled for the current page using
* chrome specific APIs.
* @private
*/
ForwardChannelRequestPool.isSpdyEnabled_ = function() {
return !!(window.chrome && window.chrome.loadTimes &&
window.chrome.loadTimes() && window.chrome.loadTimes().wasFetchedViaSpdy);
};
/**
* Once we know the client protocol (from the handshake), check if we need
* enable the request pool accordingly. This is more robust than using
* browser-internal APIs (specific to Chrome).
*
* @param {string} clientProtocol The client protocol
*/
ForwardChannelRequestPool.prototype.applyClientProtocol = function(
clientProtocol) {
if (this.requestPool_) {
return;
}
if (goog.string.contains(clientProtocol, 'spdy') ||
goog.string.contains(clientProtocol, 'quic')) {
this.maxSize_ = this.maxPoolSizeConfigured_;
this.requestPool_ = new goog.structs.Set();
if (this.request_) {
this.addRequest(this.request_);
this.request_ = null;
}
}
};
/**
* @return {boolean} True if the pool is full.
*/
ForwardChannelRequestPool.prototype.isFull = function() {
if (this.request_) {
return true;
}
if (this.requestPool_) {
return this.requestPool_.getCount() >= this.maxSize_;
}
return false;
};
/**
* @return {number} The current size limit.
*/
ForwardChannelRequestPool.prototype.getMaxSize = function() {
return this.maxSize_;
};
/**
* @return {number} The number of pending requests in the pool.
*/
ForwardChannelRequestPool.prototype.getRequestCount = function() {
if (this.request_) {
return 1;
}
if (this.requestPool_) {
return this.requestPool_.getCount();
}
return 0;
};
/**
* @param {ChannelRequest} req The channel request.
* @return {boolean} True if the request is a included inside the pool.
*/
ForwardChannelRequestPool.prototype.hasRequest = function(req) {
if (this.request_) {
return this.request_ == req;
}
if (this.requestPool_) {
return this.requestPool_.contains(req);
}
return false;
};
/**
* Adds a new request to the pool.
*
* @param {!ChannelRequest} req The new channel request.
*/
ForwardChannelRequestPool.prototype.addRequest = function(req) {
if (this.requestPool_) {
this.requestPool_.add(req);
} else {
this.request_ = req;
}
};
/**
* Removes the given request from the pool.
*
* @param {ChannelRequest} req The channel request.
* @return {boolean} Whether the request has been removed from the pool.
*/
ForwardChannelRequestPool.prototype.removeRequest = function(req) {
if (this.request_ && this.request_ == req) {
this.request_ = null;
return true;
}
if (this.requestPool_ && this.requestPool_.contains(req)) {
this.requestPool_.remove(req);
return true;
}
return false;
};
/**
* Clears the pool and cancel all the pending requests.
*/
ForwardChannelRequestPool.prototype.cancel = function() {
if (this.request_) {
this.request_.cancel();
this.request_ = null;
return;
}
if (this.requestPool_ && !this.requestPool_.isEmpty()) {
goog.array.forEach(this.requestPool_.getValues(), function(val) {
val.cancel();
});
this.requestPool_.clear();
}
};
/**
* @return {boolean} Whether there are any pending requests.
*/
ForwardChannelRequestPool.prototype.hasPendingRequest = function() {
return (this.request_ != null) ||
(this.requestPool_ != null && !this.requestPool_.isEmpty());
};
/**
* Cancels all pending requests and force the completion of channel requests.
*
* Need go through the standard onRequestComplete logic to expose the max-retry
* failure in the standard way.
*
* @param {!function(!ChannelRequest)} onComplete The completion callback.
* @return {boolean} true if any request has been forced to complete.
*/
ForwardChannelRequestPool.prototype.forceComplete = function(onComplete) {
if (this.request_ != null) {
this.request_.cancel();
onComplete(this.request_);
return true;
}
if (this.requestPool_ && !this.requestPool_.isEmpty()) {
goog.array.forEach(this.requestPool_.getValues(),
function(val) {
val.cancel();
onComplete(val);
});
return true;
}
return false;
};
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.ForwardChannelRequestPool</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.forwardChannelRequestPoolTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,119 @@
// Copyright 2013 The Closure Library 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 Unit tests for
* goog.labs.net.webChannel.ForwardChannelRequestPool.
* @suppress {accessControls} Private methods are accessed for test purposes.
*
*/
goog.provide('goog.labs.net.webChannel.forwardChannelRequestPoolTest');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.ForwardChannelRequestPool');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.net.webChannel.forwardChannelRequestPoolTest');
var propertyReplacer = new goog.testing.PropertyReplacer();
var req = new goog.labs.net.webChannel.ChannelRequest(null, null);
function setUp() {
}
function tearDown() {
propertyReplacer.reset();
}
function stubSpdyCheck(spdyEnabled) {
propertyReplacer.set(goog.labs.net.webChannel.ForwardChannelRequestPool,
'isSpdyEnabled_',
function() {
return spdyEnabled;
});
}
function testSpdyEnabled() {
stubSpdyCheck(true);
var pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertFalse(pool.isFull());
assertEquals(0, pool.getRequestCount());
pool.addRequest(req);
assertTrue(pool.hasPendingRequest());
assertTrue(pool.hasRequest(req));
pool.removeRequest(req);
assertFalse(pool.hasPendingRequest());
for (var i = 0; i < pool.getMaxSize(); i++) {
pool.addRequest(new goog.labs.net.webChannel.ChannelRequest(null, null));
}
assertTrue(pool.isFull());
// do not fail
pool.addRequest(req);
assertTrue(pool.isFull());
}
function testSpdyNotEnabled() {
stubSpdyCheck(false);
var pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertFalse(pool.isFull());
assertEquals(0, pool.getRequestCount());
pool.addRequest(req);
assertTrue(pool.hasPendingRequest());
assertTrue(pool.hasRequest(req));
assertTrue(pool.isFull());
pool.removeRequest(req);
assertFalse(pool.hasPendingRequest());
// do not fail
pool.addRequest(req);
assertTrue(pool.isFull());
}
function testApplyClientProtocol() {
stubSpdyCheck(false);
var pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertEquals(1, pool.getMaxSize());
pool.applyClientProtocol('spdy/3');
assertTrue(pool.getMaxSize() > 1);
pool.applyClientProtocol('foo-bar'); // no effect
assertTrue(pool.getMaxSize() > 1);
pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertEquals(1, pool.getMaxSize());
pool.applyClientProtocol('quic/x');
assertTrue(pool.getMaxSize() > 1);
stubSpdyCheck(true);
pool = new goog.labs.net.webChannel.ForwardChannelRequestPool();
assertTrue(pool.getMaxSize() > 1);
pool.applyClientProtocol('foo/3'); // no effect
assertTrue(pool.getMaxSize() > 1);
}
@@ -0,0 +1,162 @@
// Copyright 2013 The Closure Library 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 Utility functions for managing networking, such as
* testing network connectivity.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.netUtils');
goog.require('goog.Uri');
goog.require('goog.labs.net.webChannel.WebChannelDebug');
goog.scope(function() {
var netUtils = goog.labs.net.webChannel.netUtils;
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
/**
* Default timeout to allow for URI pings.
* @type {number}
*/
netUtils.NETWORK_TIMEOUT = 10000;
/**
* Pings the network with an image URI to check if an error is a server error
* or user's network error.
*
* The caller needs to add a 'rand' parameter to make sure the response is
* not fulfilled by browser cache.
*
* @param {function(boolean)} callback The function to call back with results.
* @param {goog.Uri=} opt_imageUri The URI (of an image) to use for the network
* test.
*/
netUtils.testNetwork = function(callback, opt_imageUri) {
var uri = opt_imageUri;
if (!uri) {
// default google.com image
uri = new goog.Uri('//www.google.com/images/cleardot.gif');
if (!(goog.global.location && goog.global.location.protocol == 'http')) {
uri.setScheme('https'); // e.g. chrome-extension
}
uri.makeUnique();
}
netUtils.testLoadImage(uri.toString(), netUtils.NETWORK_TIMEOUT, callback);
};
/**
* Test loading the given image, retrying if necessary.
* @param {string} url URL to the image.
* @param {number} timeout Milliseconds before giving up.
* @param {function(boolean)} callback Function to call with results.
* @param {number} retries The number of times to retry.
* @param {number=} opt_pauseBetweenRetriesMS Optional number of milliseconds
* between retries - defaults to 0.
*/
netUtils.testLoadImageWithRetries = function(url, timeout, callback,
retries, opt_pauseBetweenRetriesMS) {
var channelDebug = new WebChannelDebug();
channelDebug.debug('TestLoadImageWithRetries: ' + opt_pauseBetweenRetriesMS);
if (retries == 0) {
// no more retries, give up
callback(false);
return;
}
var pauseBetweenRetries = opt_pauseBetweenRetriesMS || 0;
retries--;
netUtils.testLoadImage(url, timeout, function(succeeded) {
if (succeeded) {
callback(true);
} else {
// try again
goog.global.setTimeout(function() {
netUtils.testLoadImageWithRetries(url, timeout, callback,
retries, pauseBetweenRetries);
}, pauseBetweenRetries);
}
});
};
/**
* Test loading the given image.
* @param {string} url URL to the image.
* @param {number} timeout Milliseconds before giving up.
* @param {function(boolean)} callback Function to call with results.
*/
netUtils.testLoadImage = function(url, timeout, callback) {
var channelDebug = new WebChannelDebug();
channelDebug.debug('TestLoadImage: loading ' + url);
var img = new Image();
img.onload = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: loaded', true, callback);
img.onerror = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: error', false, callback);
img.onabort = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: abort', false, callback);
img.ontimeout = goog.partial(netUtils.imageCallback_, channelDebug, img,
'TestLoadImage: timeout', false, callback);
goog.global.setTimeout(function() {
if (img.ontimeout) {
img.ontimeout();
}
}, timeout);
img.src = url;
};
/**
* Wrap the image callback with debug and cleanup logic.
* @param {!WebChannelDebug} channelDebug The WebChannelDebug object.
* @param {!Image} img The image element.
* @param {string} debugText The debug text.
* @param {boolean} result The result of image loading.
* @param {function(boolean)} callback The image callback.
* @private
*/
netUtils.imageCallback_ = function(channelDebug, img, debugText, result,
callback) {
try {
channelDebug.debug(debugText);
netUtils.clearImageCallbacks_(img);
callback(result);
} catch (e) {
channelDebug.dumpException(e);
}
};
/**
* Clears handlers to avoid memory leaks.
* @param {Image} img The image to clear handlers from.
* @private
*/
netUtils.clearImageCallbacks_ = function(img) {
img.onload = null;
img.onerror = null;
img.onabort = null;
img.ontimeout = null;
};
}); // goog.scope
@@ -0,0 +1,386 @@
// Copyright 2013 The Closure Library 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 Static utilities for collecting stats associated with
* ChannelRequest.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.requestStats');
goog.provide('goog.labs.net.webChannel.requestStats.Event');
goog.provide('goog.labs.net.webChannel.requestStats.ServerReachability');
goog.provide('goog.labs.net.webChannel.requestStats.ServerReachabilityEvent');
goog.provide('goog.labs.net.webChannel.requestStats.Stat');
goog.provide('goog.labs.net.webChannel.requestStats.StatEvent');
goog.provide('goog.labs.net.webChannel.requestStats.TimingEvent');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.scope(function() {
var requestStats = goog.labs.net.webChannel.requestStats;
/**
* Events fired.
* @const
*/
requestStats.Event = {};
/**
* Singleton event target for firing stat events
* @type {goog.events.EventTarget}
* @private
*/
requestStats.statEventTarget_ = new goog.events.EventTarget();
/**
* The type of event that occurs every time some information about how reachable
* the server is is discovered.
*/
requestStats.Event.SERVER_REACHABILITY_EVENT = 'serverreachability';
/**
* Types of events which reveal information about the reachability of the
* server.
* @enum {number}
*/
requestStats.ServerReachability = {
REQUEST_MADE: 1,
REQUEST_SUCCEEDED: 2,
REQUEST_FAILED: 3,
BACK_CHANNEL_ACTIVITY: 4
};
/**
* Event class for SERVER_REACHABILITY_EVENT.
*
* @param {goog.events.EventTarget} target The stat event target for
the channel.
* @param {requestStats.ServerReachability} reachabilityType
* The reachability event type.
* @constructor
* @extends {goog.events.Event}
*/
requestStats.ServerReachabilityEvent = function(target, reachabilityType) {
goog.events.Event.call(this,
requestStats.Event.SERVER_REACHABILITY_EVENT, target);
/**
* @type {requestStats.ServerReachability}
*/
this.reachabilityType = reachabilityType;
};
goog.inherits(requestStats.ServerReachabilityEvent, goog.events.Event);
/**
* Notify the channel that a particular fine grained network event has occurred.
* Should be considered package-private.
* @param {requestStats.ServerReachability} reachabilityType
* The reachability event type.
*/
requestStats.notifyServerReachabilityEvent = function(reachabilityType) {
var target = requestStats.statEventTarget_;
target.dispatchEvent(
new requestStats.ServerReachabilityEvent(target, reachabilityType));
};
/**
* Stat Event that fires when things of interest happen that may be useful for
* applications to know about for stats or debugging purposes.
*/
requestStats.Event.STAT_EVENT = 'statevent';
/**
* Enum that identifies events for statistics that are interesting to track.
* @enum {number}
*/
requestStats.Stat = {
/** Event indicating a new connection attempt. */
CONNECT_ATTEMPT: 0,
/** Event indicating a connection error due to a general network problem. */
ERROR_NETWORK: 1,
/**
* Event indicating a connection error that isn't due to a general network
* problem.
*/
ERROR_OTHER: 2,
/** Event indicating the start of test stage one. */
TEST_STAGE_ONE_START: 3,
/** Event indicating the start of test stage two. */
TEST_STAGE_TWO_START: 4,
/** Event indicating the first piece of test data was received. */
TEST_STAGE_TWO_DATA_ONE: 5,
/**
* Event indicating that the second piece of test data was received and it was
* recieved separately from the first.
*/
TEST_STAGE_TWO_DATA_TWO: 6,
/** Event indicating both pieces of test data were received simultaneously. */
TEST_STAGE_TWO_DATA_BOTH: 7,
/** Event indicating stage one of the test request failed. */
TEST_STAGE_ONE_FAILED: 8,
/** Event indicating stage two of the test request failed. */
TEST_STAGE_TWO_FAILED: 9,
/**
* Event indicating that a buffering proxy is likely between the client and
* the server.
*/
PROXY: 10,
/**
* Event indicating that no buffering proxy is likely between the client and
* the server.
*/
NOPROXY: 11,
/** Event indicating an unknown SID error. */
REQUEST_UNKNOWN_SESSION_ID: 12,
/** Event indicating a bad status code was received. */
REQUEST_BAD_STATUS: 13,
/** Event indicating incomplete data was received */
REQUEST_INCOMPLETE_DATA: 14,
/** Event indicating bad data was received */
REQUEST_BAD_DATA: 15,
/** Event indicating no data was received when data was expected. */
REQUEST_NO_DATA: 16,
/** Event indicating a request timeout. */
REQUEST_TIMEOUT: 17,
/**
* Event indicating that the server never received our hanging GET and so it
* is being retried.
*/
BACKCHANNEL_MISSING: 18,
/**
* Event indicating that we have determined that our hanging GET is not
* receiving data when it should be. Thus it is dead dead and will be retried.
*/
BACKCHANNEL_DEAD: 19,
/**
* The browser declared itself offline during the lifetime of a request, or
* was offline when a request was initially made.
*/
BROWSER_OFFLINE: 20,
/** ActiveX is blocked by the machine's admin settings. */
ACTIVE_X_BLOCKED: 21
};
/**
* Event class for STAT_EVENT.
*
* @param {goog.events.EventTarget} eventTarget The stat event target for
the channel.
* @param {requestStats.Stat} stat The stat.
* @constructor
* @extends {goog.events.Event}
*/
requestStats.StatEvent = function(eventTarget, stat) {
goog.events.Event.call(this, requestStats.Event.STAT_EVENT, eventTarget);
/**
* The stat
* @type {requestStats.Stat}
*/
this.stat = stat;
};
goog.inherits(requestStats.StatEvent, goog.events.Event);
/**
* Returns the singleton event target for stat events.
* @return {goog.events.EventTarget} The event target for stat events.
*/
requestStats.getStatEventTarget = function() {
return requestStats.statEventTarget_;
};
/**
* Helper function to call the stat event callback.
* @param {requestStats.Stat} stat The stat.
*/
requestStats.notifyStatEvent = function(stat) {
var target = requestStats.statEventTarget_;
target.dispatchEvent(new requestStats.StatEvent(target, stat));
};
/**
* An event that fires when POST requests complete successfully, indicating
* the size of the POST and the round trip time.
*/
requestStats.Event.TIMING_EVENT = 'timingevent';
/**
* Event class for requestStats.Event.TIMING_EVENT
*
* @param {goog.events.EventTarget} target The stat event target for
the channel.
* @param {number} size The number of characters in the POST data.
* @param {number} rtt The total round trip time from POST to response in MS.
* @param {number} retries The number of times the POST had to be retried.
* @constructor
* @extends {goog.events.Event}
*/
requestStats.TimingEvent = function(target, size, rtt, retries) {
goog.events.Event.call(this,
requestStats.Event.TIMING_EVENT, target);
/**
* @type {number}
*/
this.size = size;
/**
* @type {number}
*/
this.rtt = rtt;
/**
* @type {number}
*/
this.retries = retries;
};
goog.inherits(requestStats.TimingEvent, goog.events.Event);
/**
* Helper function to notify listeners about POST request performance.
*
* @param {number} size Number of characters in the POST data.
* @param {number} rtt The amount of time from POST start to response.
* @param {number} retries The number of times the POST had to be retried.
*/
requestStats.notifyTimingEvent = function(size, rtt, retries) {
var target = requestStats.statEventTarget_;
target.dispatchEvent(
new requestStats.TimingEvent(
target, size, rtt, retries));
};
/**
* Allows the application to set an execution hooks for when a channel
* starts processing requests. This is useful to track timing or logging
* special information. The function takes no parameters and return void.
* @param {Function} startHook The function for the start hook.
*/
requestStats.setStartThreadExecutionHook = function(startHook) {
requestStats.startExecutionHook_ = startHook;
};
/**
* Allows the application to set an execution hooks for when a channel
* stops processing requests. This is useful to track timing or logging
* special information. The function takes no parameters and return void.
* @param {Function} endHook The function for the end hook.
*/
requestStats.setEndThreadExecutionHook = function(endHook) {
requestStats.endExecutionHook_ = endHook;
};
/**
* Application provided execution hook for the start hook.
*
* @type {Function}
* @private
*/
requestStats.startExecutionHook_ = function() { };
/**
* Application provided execution hook for the end hook.
*
* @type {Function}
* @private
*/
requestStats.endExecutionHook_ = function() { };
/**
* Helper function to call the start hook
*/
requestStats.onStartExecution = function() {
requestStats.startExecutionHook_();
};
/**
* Helper function to call the end hook
*/
requestStats.onEndExecution = function() {
requestStats.endExecutionHook_();
};
/**
* Wrapper around SafeTimeout which calls the start and end execution hooks
* with a try...finally block.
* @param {Function} fn The callback function.
* @param {number} ms The time in MS for the timer.
* @return {number} The ID of the timer.
*/
requestStats.setTimeout = function(fn, ms) {
if (!goog.isFunction(fn)) {
throw Error('Fn must not be null and must be a function');
}
return goog.global.setTimeout(function() {
requestStats.onStartExecution();
try {
fn();
} finally {
requestStats.onEndExecution();
}
}, ms);
};
}); // goog.scope
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.WebChannelBase</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.webChannelBaseTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,376 @@
// Copyright 2013 The Closure Library 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 Implementation of a WebChannel transport using WebChannelBase.
*
* When WebChannelBase is used as the underlying transport, the capabilities
* of the WebChannel are limited to what's supported by the implementation.
* Particularly, multiplexing is not possible, and only strings are
* supported as message types.
*
*/
goog.provide('goog.labs.net.webChannel.WebChannelBaseTransport');
goog.require('goog.asserts');
goog.require('goog.events.EventTarget');
goog.require('goog.labs.net.webChannel.WebChannelBase');
goog.require('goog.log');
goog.require('goog.net.WebChannel');
goog.require('goog.net.WebChannelTransport');
goog.require('goog.object');
goog.require('goog.string.path');
/**
* Implementation of {@link goog.net.WebChannelTransport} with
* {@link goog.labs.net.webChannel.WebChannelBase} as the underlying channel
* implementation.
*
* @constructor
* @struct
* @implements {goog.net.WebChannelTransport}
* @final
*/
goog.labs.net.webChannel.WebChannelBaseTransport = function() {};
goog.scope(function() {
var WebChannelBaseTransport = goog.labs.net.webChannel.WebChannelBaseTransport;
var WebChannelBase = goog.labs.net.webChannel.WebChannelBase;
/**
* @override
*/
WebChannelBaseTransport.prototype.createWebChannel = function(
url, opt_options) {
return new WebChannelBaseTransport.Channel(url, opt_options);
};
/**
* Implementation of the {@link goog.net.WebChannel} interface.
*
* @param {string} url The URL path for the new WebChannel instance.
* @param {!goog.net.WebChannel.Options=} opt_options Configuration for the
* new WebChannel instance.
*
* @constructor
* @implements {goog.net.WebChannel}
* @extends {goog.events.EventTarget}
* @final
*/
WebChannelBaseTransport.Channel = function(url, opt_options) {
WebChannelBaseTransport.Channel.base(this, 'constructor');
/**
* The underlying channel object.
*
* @private {!WebChannelBase}
*/
this.channel_ = new WebChannelBase(opt_options);
/**
* The URL of the target server end-point.
*
* @private {string}
*/
this.url_ = url;
/**
* The test URL of the target server end-point. This value defaults to
* this.url_ + '/test'.
*
* @private {string}
*/
this.testUrl_ = (opt_options && opt_options.testUrl) ? opt_options.testUrl :
goog.string.path.join(this.url_, 'test');
/**
* The logger for this class.
* @private {goog.log.Logger}
*/
this.logger_ = goog.log.getLogger(
'goog.labs.net.webChannel.WebChannelBaseTransport');
/**
* @private {Object<string, string>} messageUrlParams_ Extra URL parameters
* to be added to each HTTP request.
*/
this.messageUrlParams_ =
(opt_options && opt_options.messageUrlParams) || null;
var messageHeaders = (opt_options && opt_options.messageHeaders) || null;
// default is false
if (opt_options && opt_options.clientProtocolHeaderRequired) {
if (messageHeaders) {
goog.object.set(messageHeaders,
goog.net.WebChannel.X_CLIENT_PROTOCOL,
goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL);
} else {
messageHeaders = goog.object.create(
goog.net.WebChannel.X_CLIENT_PROTOCOL,
goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL);
}
}
this.channel_.setExtraHeaders(messageHeaders);
/**
* @private {boolean} supportsCrossDomainXhr_ Whether to enable CORS.
*/
this.supportsCrossDomainXhr_ =
(opt_options && opt_options.supportsCrossDomainXhr) || false;
};
goog.inherits(WebChannelBaseTransport.Channel, goog.events.EventTarget);
/**
* The channel handler.
*
* @type {WebChannelBase.Handler}
* @private
*/
WebChannelBaseTransport.Channel.prototype.channelHandler_ = null;
/**
* Test path is always set to "/url/test".
*
* @override
*/
WebChannelBaseTransport.Channel.prototype.open = function() {
this.channel_.connect(this.testUrl_, this.url_,
(this.messageUrlParams_ || undefined));
this.channelHandler_ = new WebChannelBaseTransport.Channel.Handler_(this);
this.channel_.setHandler(this.channelHandler_);
if (this.supportsCrossDomainXhr_) {
this.channel_.setSupportsCrossDomainXhrs(true);
}
};
/**
* @override
*/
WebChannelBaseTransport.Channel.prototype.close = function() {
this.channel_.disconnect();
};
/**
* The WebChannelBase only supports object types.
*
* @param {!goog.net.WebChannel.MessageData} message The message to send.
* @override
*/
WebChannelBaseTransport.Channel.prototype.send = function(message) {
goog.asserts.assert(goog.isObject(message), 'only object type expected');
this.channel_.sendMap(message);
};
/**
* @override
*/
WebChannelBaseTransport.Channel.prototype.disposeInternal = function() {
this.channel_.setHandler(null);
delete this.channelHandler_;
this.channel_.disconnect();
delete this.channel_;
WebChannelBaseTransport.Channel.base(this, 'disposeInternal');
};
/**
* The message event.
*
* @param {!Array<?>} array The data array from the underlying channel.
* @constructor
* @extends {goog.net.WebChannel.MessageEvent}
* @final
*/
WebChannelBaseTransport.Channel.MessageEvent = function(array) {
WebChannelBaseTransport.Channel.MessageEvent.base(this, 'constructor');
this.data = array;
};
goog.inherits(WebChannelBaseTransport.Channel.MessageEvent,
goog.net.WebChannel.MessageEvent);
/**
* The error event.
*
* @param {WebChannelBase.Error} error The error code.
* @constructor
* @extends {goog.net.WebChannel.ErrorEvent}
* @final
*/
WebChannelBaseTransport.Channel.ErrorEvent = function(error) {
WebChannelBaseTransport.Channel.ErrorEvent.base(this, 'constructor');
/**
* Transport specific error code is not to be propagated with the event.
*/
this.status = goog.net.WebChannel.ErrorStatus.NETWORK_ERROR;
};
goog.inherits(WebChannelBaseTransport.Channel.ErrorEvent,
goog.net.WebChannel.ErrorEvent);
/**
* Implementation of {@link WebChannelBase.Handler} interface.
*
* @param {!WebChannelBaseTransport.Channel} channel The enclosing WebChannel.
*
* @constructor
* @extends {WebChannelBase.Handler}
* @private
*/
WebChannelBaseTransport.Channel.Handler_ = function(channel) {
WebChannelBaseTransport.Channel.Handler_.base(this, 'constructor');
/**
* @type {!WebChannelBaseTransport.Channel}
* @private
*/
this.channel_ = channel;
};
goog.inherits(WebChannelBaseTransport.Channel.Handler_, WebChannelBase.Handler);
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelOpened = function(
channel) {
goog.log.info(this.channel_.logger_,
'WebChannel opened on ' + this.channel_.url_);
this.channel_.dispatchEvent(goog.net.WebChannel.EventType.OPEN);
};
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelHandleArray =
function(channel, array) {
goog.asserts.assert(array, 'array expected to be defined');
this.channel_.dispatchEvent(
new WebChannelBaseTransport.Channel.MessageEvent(array));
};
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelError = function(
channel, error) {
goog.log.info(this.channel_.logger_,
'WebChannel aborted on ' + this.channel_.url_ +
' due to channel error: ' + error);
this.channel_.dispatchEvent(
new WebChannelBaseTransport.Channel.ErrorEvent(error));
};
/**
* @override
*/
WebChannelBaseTransport.Channel.Handler_.prototype.channelClosed = function(
channel, opt_pendingMaps, opt_undeliveredMaps) {
goog.log.info(this.channel_.logger_,
'WebChannel closed on ' + this.channel_.url_);
this.channel_.dispatchEvent(goog.net.WebChannel.EventType.CLOSE);
};
/**
* @override
*/
WebChannelBaseTransport.Channel.prototype.getRuntimeProperties = function() {
return new WebChannelBaseTransport.ChannelProperties(this.channel_);
};
/**
* Implementation of the {@link goog.net.WebChannel.RuntimeProperties}.
*
* @param {!WebChannelBase} channel The underlying channel object.
*
* @constructor
* @implements {goog.net.WebChannel.RuntimeProperties}
* @final
*/
WebChannelBaseTransport.ChannelProperties = function(channel) {
/**
* The underlying channel object.
*
* @private {!WebChannelBase}
*/
this.channel_ = channel;
/**
* The flag to turn on/off server-side flow control.
*
* @private {boolean}
*/
this.serverFlowControlEnabled_ = false;
};
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.getConcurrentRequestLimit =
function() {
return this.channel_.getForwardChannelRequestPool().getMaxSize();
};
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.isSpdyEnabled =
function() {
return this.getConcurrentRequestLimit() > 1;
};
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.setServerFlowControl =
goog.abstractMethod;
/**
* @override
*/
WebChannelBaseTransport.ChannelProperties.prototype.getNonAckedMessageCount =
goog.abstractMethod;
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.WebChannelBaseTransport</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.webChannelBaseTransportTest');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,254 @@
// Copyright 2013 The Closure Library 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 Unit tests for goog.labs.net.webChannel.WebChannelBase.
* @suppress {accessControls} Private methods are accessed for test purposes.
*
*/
goog.provide('goog.labs.net.webChannel.webChannelBaseTransportTest');
goog.require('goog.events');
goog.require('goog.labs.net.webChannel.WebChannelBaseTransport');
goog.require('goog.net.WebChannel');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.net.webChannel.webChannelBaseTransportTest');
var webChannel;
var channelUrl = 'http://127.0.0.1:8080/channel';
function setUp() {
}
function tearDown() {
goog.dispose(webChannel);
}
function testOpenWithUrl() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
goog.events.listen(webChannel, goog.net.WebChannel.EventType.OPEN,
function(e) {
eventFired = true;
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateOpenEvent(channel);
assertTrue(eventFired);
}
function testOpenWithTestUrl() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'testUrl': channelUrl + '/footest'};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var testPath = webChannel.channel_.connectionTest_.path_;
assertNotNullNorUndefined(testPath);
}
function testOpenWithCustomHeaders() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'messageHeaders': {'foo-key': 'foo-value'}};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNotNullNorUndefined(extraHeaders_);
assertEquals('foo-value', extraHeaders_['foo-key']);
assertEquals(undefined, extraHeaders_['X-Client-Protocol']);
}
function testClientProtocolHeaderRequired() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'clientProtocolHeaderRequired': true};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNotNullNorUndefined(extraHeaders_);
assertEquals('webchannel', extraHeaders_['X-Client-Protocol']);
}
function testClientProtocolHeaderNotRequiredByDefault() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNull(extraHeaders_);
}
function testClientProtocolHeaderRequiredWithCustomHeader() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {
'clientProtocolHeaderRequired': true,
'messageHeaders': {'foo-key': 'foo-value'}
};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraHeaders_ = webChannel.channel_.extraHeaders_;
assertNotNullNorUndefined(extraHeaders_);
assertEquals('foo-value', extraHeaders_['foo-key']);
assertEquals('webchannel', extraHeaders_['X-Client-Protocol']);
}
function testOpenWithCustomParams() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'messageUrlParams': {'foo-key': 'foo-value'}};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
var extraParams = webChannel.channel_.extraParams_;
assertNotNullNorUndefined(extraParams);
}
function testOpenWithCorsEnabled() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
var options = {'supportsCrossDomainXhr': true};
webChannel = webChannelTransport.createWebChannel(channelUrl, options);
webChannel.open();
assertTrue(webChannel.channel_.supportsCrossDomainXhrs_);
}
function testOpenThenCloseChannel() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
goog.events.listen(webChannel, goog.net.WebChannel.EventType.CLOSE,
function(e) {
eventFired = true;
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateCloseEvent(channel);
assertTrue(eventFired);
}
function testChannelError() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
goog.events.listen(webChannel, goog.net.WebChannel.EventType.ERROR,
function(e) {
eventFired = true;
assertEquals(goog.net.WebChannel.ErrorStatus.NETWORK_ERROR, e.status);
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateErrorEvent(channel);
assertTrue(eventFired);
}
function testChannelMessage() {
var webChannelTransport =
new goog.labs.net.webChannel.WebChannelBaseTransport();
webChannel = webChannelTransport.createWebChannel(channelUrl);
var eventFired = false;
var data = 'foo';
goog.events.listen(webChannel, goog.net.WebChannel.EventType.MESSAGE,
function(e) {
eventFired = true;
assertEquals(e.data, data);
});
webChannel.open();
assertFalse(eventFired);
var channel = webChannel.channel_;
assertNotNull(channel);
simulateMessageEvent(channel, data);
assertTrue(eventFired);
}
/**
* Simulates the WebChannelBase firing the open event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
*/
function simulateOpenEvent(channel) {
assertNotNull(channel.getHandler());
channel.getHandler().channelOpened();
}
/**
* Simulates the WebChannelBase firing the close event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
*/
function simulateCloseEvent(channel) {
assertNotNull(channel.getHandler());
channel.getHandler().channelClosed();
}
/**
* Simulates the WebChannelBase firing the error event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
*/
function simulateErrorEvent(channel) {
assertNotNull(channel.getHandler());
channel.getHandler().channelError();
}
/**
* Simulates the WebChannelBase firing the message event for the given channel.
* @param {!goog.labs.net.webChannel.WebChannelBase} channel The WebChannelBase.
* @param {String} data The message data.
*/
function simulateMessageEvent(channel, data) {
assertNotNull(channel.getHandler());
channel.getHandler().channelHandleArray(channel, data);
}
@@ -0,0 +1,300 @@
// Copyright 2006 The Closure Library 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 Provides a utility for tracing and debugging WebChannel
* requests.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.WebChannelDebug');
goog.require('goog.json');
goog.require('goog.log');
/**
* Logs and keeps a buffer of debugging info for the Channel.
*
* @constructor
* @struct
* @final
*/
goog.labs.net.webChannel.WebChannelDebug = function() {
/**
* The logger instance.
* @const
* @private
*/
this.logger_ = goog.log.getLogger('goog.labs.net.webChannel.WebChannelDebug');
};
goog.scope(function() {
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
/**
* Gets the logger used by this ChannelDebug.
* @return {goog.debug.Logger} The logger used by this WebChannelDebug.
*/
WebChannelDebug.prototype.getLogger = function() {
return this.logger_;
};
/**
* Logs that the browser went offline during the lifetime of a request.
* @param {goog.Uri} url The URL being requested.
*/
WebChannelDebug.prototype.browserOfflineResponse = function(url) {
this.info('BROWSER_OFFLINE: ' + url);
};
/**
* Logs an XmlHttp request..
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
* @param {?string} postData The data posted in the request.
*/
WebChannelDebug.prototype.xmlHttpChannelRequest =
function(verb, uri, id, attempt, postData) {
this.info(
'XMLHTTP REQ (' + id + ') [attempt ' + attempt + ']: ' +
verb + '\n' + uri + '\n' +
this.maybeRedactPostData_(postData));
};
/**
* Logs the meta data received from an XmlHttp request.
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
* @param {goog.net.XmlHttp.ReadyState} readyState The ready state.
* @param {number} statusCode The HTTP status code.
*/
WebChannelDebug.prototype.xmlHttpChannelResponseMetaData =
function(verb, uri, id, attempt, readyState, statusCode) {
this.info(
'XMLHTTP RESP (' + id + ') [ attempt ' + attempt + ']: ' +
verb + '\n' + uri + '\n' + readyState + ' ' + statusCode);
};
/**
* Logs the response data received from an XmlHttp request.
* @param {string|number|undefined} id The request id.
* @param {?string} responseText The response text.
* @param {?string=} opt_desc Optional request description.
*/
WebChannelDebug.prototype.xmlHttpChannelResponseText =
function(id, responseText, opt_desc) {
this.info(
'XMLHTTP TEXT (' + id + '): ' +
this.redactResponse_(responseText) +
(opt_desc ? ' ' + opt_desc : ''));
};
/**
* Logs a Trident ActiveX request.
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
*/
WebChannelDebug.prototype.tridentChannelRequest =
function(verb, uri, id, attempt) {
this.info(
'TRIDENT REQ (' + id + ') [ attempt ' + attempt + ']: ' +
verb + '\n' + uri);
};
/**
* Logs the response text received from a Trident ActiveX request.
* @param {string|number|undefined} id The request id.
* @param {string} responseText The response text.
*/
WebChannelDebug.prototype.tridentChannelResponseText =
function(id, responseText) {
this.info(
'TRIDENT TEXT (' + id + '): ' +
this.redactResponse_(responseText));
};
/**
* Logs the done response received from a Trident ActiveX request.
* @param {string|number|undefined} id The request id.
* @param {boolean} successful Whether the request was successful.
*/
WebChannelDebug.prototype.tridentChannelResponseDone =
function(id, successful) {
this.info(
'TRIDENT TEXT (' + id + '): ' + successful ? 'success' : 'failure');
};
/**
* Logs a request timeout.
* @param {goog.Uri} uri The uri that timed out.
*/
WebChannelDebug.prototype.timeoutResponse = function(uri) {
this.info('TIMEOUT: ' + uri);
};
/**
* Logs a debug message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.debug = function(text) {
this.info(text);
};
/**
* Logs an exception
* @param {Error} e The error or error event.
* @param {string=} opt_msg The optional message, defaults to 'Exception'.
*/
WebChannelDebug.prototype.dumpException = function(e, opt_msg) {
this.severe((opt_msg || 'Exception') + e);
};
/**
* Logs an info message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.info = function(text) {
goog.log.info(this.logger_, text);
};
/**
* Logs a warning message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.warning = function(text) {
goog.log.warning(this.logger_, text);
};
/**
* Logs a severe message.
* @param {string} text The message.
*/
WebChannelDebug.prototype.severe = function(text) {
goog.log.error(this.logger_, text);
};
/**
* Removes potentially private data from a response so that we don't
* accidentally save private and personal data to the server logs.
* @param {?string} responseText A JSON response to clean.
* @return {?string} The cleaned response.
* @private
*/
WebChannelDebug.prototype.redactResponse_ = function(responseText) {
if (!responseText) {
return null;
}
/** @preserveTry */
try {
var responseArray = goog.json.unsafeParse(responseText);
if (responseArray) {
for (var i = 0; i < responseArray.length; i++) {
if (goog.isArray(responseArray[i])) {
this.maybeRedactArray_(responseArray[i]);
}
}
}
return goog.json.serialize(responseArray);
} catch (e) {
this.debug('Exception parsing expected JS array - probably was not JS');
return responseText;
}
};
/**
* Removes data from a response array that may be sensitive.
* @param {!Array<?>} array The array to clean.
* @private
*/
WebChannelDebug.prototype.maybeRedactArray_ = function(array) {
if (array.length < 2) {
return;
}
var dataPart = array[1];
if (!goog.isArray(dataPart)) {
return;
}
if (dataPart.length < 1) {
return;
}
var type = dataPart[0];
if (type != 'noop' && type != 'stop') {
// redact all fields in the array
for (var i = 1; i < dataPart.length; i++) {
dataPart[i] = '';
}
}
};
/**
* Removes potentially private data from a request POST body so that we don't
* accidentally save private and personal data to the server logs.
* @param {?string} data The data string to clean.
* @return {?string} The data string with sensitive data replaced by 'redacted'.
* @private
*/
WebChannelDebug.prototype.maybeRedactPostData_ = function(data) {
if (!data) {
return null;
}
var out = '';
var params = data.split('&');
for (var i = 0; i < params.length; i++) {
var param = params[i];
var keyValue = param.split('=');
if (keyValue.length > 1) {
var key = keyValue[0];
var value = keyValue[1];
var keyParts = key.split('_');
if (keyParts.length >= 2 && keyParts[1] == 'type') {
out += key + '=' + value + '&';
} else {
out += key + '=' + 'redacted' + '&';
}
}
}
return out;
};
}); // goog.scope
@@ -0,0 +1,75 @@
// Copyright 2013 The Closure Library 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 Interface and shared data structures for implementing
* different wire protocol versions.
* @visibility {//closure/goog/bin/sizetests:__pkg__}
*/
goog.provide('goog.labs.net.webChannel.Wire');
/**
* The interface class.
*
* @interface
*/
goog.labs.net.webChannel.Wire = function() {};
goog.scope(function() {
var Wire = goog.labs.net.webChannel.Wire;
/**
* The latest protocol version that this class supports. We request this version
* from the server when opening the connection. Should match
* LATEST_CHANNEL_VERSION on the server code.
* @type {number}
*/
Wire.LATEST_CHANNEL_VERSION = 8;
/**
* Simple container class for a (mapId, map) pair.
* @param {number} mapId The id for this map.
* @param {!Object|!goog.structs.Map} map The map itself.
* @param {!Object=} opt_context The context associated with the map.
* @constructor
* @struct
*/
Wire.QueuedMap = function(mapId, map, opt_context) {
/**
* The id for this map.
* @type {number}
*/
this.mapId = mapId;
/**
* The map itself.
* @type {!Object|!goog.structs.Map}
*/
this.map = map;
/**
* The context for the map.
* @type {Object}
*/
this.context = opt_context || null;
};
}); // goog.scope
@@ -0,0 +1,136 @@
// Copyright 2013 The Closure Library 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 Codec functions of the v8 wire protocol. Eventually we'd want
* to support pluggable wire-format to improve wire efficiency and to enable
* binary encoding. Such support will require an interface class, which
* will be added later.
*
* @visibility {:internal}
*/
goog.provide('goog.labs.net.webChannel.WireV8');
goog.require('goog.asserts');
goog.require('goog.json');
goog.require('goog.json.NativeJsonProcessor');
goog.require('goog.structs');
/**
* The v8 codec class.
*
* @constructor
* @struct
*/
goog.labs.net.webChannel.WireV8 = function() {
/**
* Parser for a response payload. The parser should return an array.
* @private {!goog.string.Parser}
*/
this.parser_ = new goog.json.NativeJsonProcessor();
};
goog.scope(function() {
var WireV8 = goog.labs.net.webChannel.WireV8;
var Wire = goog.labs.net.webChannel.Wire;
/**
* Encodes a standalone message into the wire format.
*
* May throw exception if the message object contains any invalid elements.
*
* @param {!Object|!goog.structs.Map} message The message data.
* V8 only support JS objects (or Map).
* @param {!Array<string>} buffer The text buffer to write the message to.
* @param {string=} opt_prefix The prefix for each field of the object.
*/
WireV8.prototype.encodeMessage = function(message, buffer, opt_prefix) {
var prefix = opt_prefix || '';
try {
goog.structs.forEach(message, function(value, key) {
var encodedValue = value;
if (goog.isObject(value)) {
encodedValue = goog.json.serialize(value);
} // keep the fast-path for primitive types
buffer.push(prefix + key + '=' + encodeURIComponent(encodedValue));
});
} catch (ex) {
// We send a map here because lots of the retry logic relies on map IDs,
// so we have to send something (possibly redundant).
buffer.push(prefix + 'type' + '=' + encodeURIComponent('_badmap'));
throw ex;
}
};
/**
* Encodes all the buffered messages of the forward channel.
*
* @param {!Array<Wire.QueuedMap>} messageQueue The message data.
* V8 only support JS objects.
* @param {number} count The number of messages to be encoded.
* @param {?function(!Object)} badMapHandler Callback for bad messages.
*/
WireV8.prototype.encodeMessageQueue = function(messageQueue, count,
badMapHandler) {
var sb = ['count=' + count];
var offset;
if (count > 0) {
// To save a bit of bandwidth, specify the base mapId and the rest as
// offsets from it.
offset = messageQueue[0].mapId;
sb.push('ofs=' + offset);
} else {
offset = 0;
}
for (var i = 0; i < count; i++) {
var mapId = messageQueue[i].mapId;
var map = messageQueue[i].map;
mapId -= offset;
try {
this.encodeMessage(map, sb, 'req' + mapId + '_');
} catch (ex) {
if (badMapHandler) {
badMapHandler(map);
}
}
}
return sb.join('&');
};
/**
* Decodes a standalone message received from the wire. May throw exception
* if text is ill-formatted.
*
* Must be valid JSON as it is insecure to use eval() to decode JS literals;
* and eval() is disallowed in Chrome apps too.
*
* Invalid JS literals include null array elements, quotas etc.
*
* @param {string} messageText The string content as received from the wire.
* @return {*} The decoded message object.
*/
WireV8.prototype.decodeMessage = function(messageText) {
var response = this.parser_.parse(messageText);
goog.asserts.assert(goog.isArray(response)); // throw exception
return response;
};
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.labs.net.webChannel.WireV8</title>
<script src="../../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.labs.net.webChannel.WireV8Test');
</script>
<div id="debug" style="font-size: small"></div>
</body>
</html>
@@ -0,0 +1,99 @@
// Copyright 2013 The Closure Library 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 Unit tests for goog.labs.net.webChannel.WireV8.
*
*/
goog.provide('goog.labs.net.webChannel.WireV8Test');
goog.require('goog.labs.net.webChannel.WireV8');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.net.webChannel.WireV8Test');
var wireCodec;
function setUp() {
wireCodec = new goog.labs.net.webChannel.WireV8();
}
function tearDown() {
}
function testEncodeSimpleMessage() {
// scalar types only
var message = {
a: 'a',
b: 'b'
};
var buff = [];
wireCodec.encodeMessage(message, buff, 'prefix_');
assertEquals(2, buff.length);
assertEquals('prefix_a=a', buff[0]);
assertEquals('prefix_b=b', buff[1]);
}
function testEncodeComplexMessage() {
var message = {
a: 'a',
b: {
x: 1,
y: 2
}
};
var buff = [];
wireCodec.encodeMessage(message, buff, 'prefix_');
assertEquals(2, buff.length);
assertEquals('prefix_a=a', buff[0]);
// a round-trip URI codec
assertEquals('prefix_b={\"x\":1,\"y\":2}', decodeURIComponent(buff[1]));
}
function testEncodeMessageQueue() {
var message1 = {
a: 'a'
};
var queuedMessage1 = {
map: message1,
mapId: 3
};
var message2 = {
b: 'b'
};
var queuedMessage2 = {
map: message2,
mapId: 4
};
var queue = [queuedMessage1, queuedMessage2];
var result = wireCodec.encodeMessageQueue(queue, 2, null);
assertEquals('count=2&ofs=3&req0_a=a&req1_b=b', result);
}
function testDecodeMessage() {
var message = wireCodec.decodeMessage('[{"a":"a", "x":1}, {"b":"b"}]');
assertTrue(goog.isArray(message));
assertEquals(2, message.length);
assertEquals('a', message[0].a);
assertEquals(1, message[0].x);
assertEquals('b', message[1].b);
}
@@ -0,0 +1,75 @@
// Copyright 2013 The Closure Library 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 Transport support for WebChannel.
*
* The <code>WebChannelTransport</code> implementation serves as the factory
* for <code>WebChannel</code>, which offers an abstraction for
* point-to-point socket-like communication similar to what BrowserChannel
* or HTML5 WebSocket offers.
*
*/
goog.provide('goog.net.WebChannelTransport');
/**
* A WebChannelTransport instance represents a shared context of logical
* connectivity between a browser client and a remote origin.
*
* Over a single WebChannelTransport instance, multiple WebChannels may be
* created against different URLs, which may all share the same
* underlying connectivity (i.e. TCP connection) whenever possible.
*
* When multi-domains are supported, such as CORS, multiple origins may be
* supported over a single WebChannelTransport instance at the same time.
*
* Sharing between different window contexts such as tabs is not addressed
* by WebChannelTransport. Applications may choose HTML5 shared workers
* or other techniques to access the same transport instance
* across different window contexts.
*
* @interface
*/
goog.net.WebChannelTransport = function() {};
/**
* The latest protocol version. The protocol version is requested
* from the server which is responsible for terminating the underlying
* wire protocols.
*
* @const
* @type {number}
* @private
*/
goog.net.WebChannelTransport.LATEST_VERSION_ = 0;
/**
* Create a new WebChannel instance.
*
* The new WebChannel is to be opened against the server-side resource
* as specified by the given URL. See {@link goog.net.WebChannel} for detailed
* semantics.
*
* @param {string} url The URL path for the new WebChannel instance.
* @param {!goog.net.WebChannel.Options=} opt_options Configuration for the
* new WebChannel instance. The configuration object is reusable after
* the new channel instance is created.
* @return {!goog.net.WebChannel} the newly created WebChannel instance.
*/
goog.net.WebChannelTransport.prototype.createWebChannel = goog.abstractMethod;
@@ -0,0 +1,35 @@
// Copyright 2013 The Closure Library 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 Default factory for <code>WebChannelTransport</code> to
* avoid exposing concrete classes to clients.
*
*/
goog.provide('goog.net.createWebChannelTransport');
goog.require('goog.functions');
goog.require('goog.labs.net.webChannel.WebChannelBaseTransport');
/**
* Create a new WebChannelTransport instance using the default implementation.
*
* @return {!goog.net.WebChannelTransport} the newly created transport instance.
*/
goog.net.createWebChannelTransport =
/** @type {function(): !goog.net.WebChannelTransport} */ (
goog.partial(goog.functions.create,
goog.labs.net.webChannel.WebChannelBaseTransport));
@@ -0,0 +1,468 @@
// Copyright 2011 The Closure Library 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 Offered as an alternative to XhrIo as a way for making requests
* via XMLHttpRequest. Instead of mirroring the XHR interface and exposing
* events, results are used as a way to pass a "promise" of the response to
* interested parties.
*
*/
goog.provide('goog.labs.net.xhr');
goog.provide('goog.labs.net.xhr.Error');
goog.provide('goog.labs.net.xhr.HttpError');
goog.provide('goog.labs.net.xhr.Options');
goog.provide('goog.labs.net.xhr.PostData');
goog.provide('goog.labs.net.xhr.ResponseType');
goog.provide('goog.labs.net.xhr.TimeoutError');
goog.require('goog.Promise');
goog.require('goog.debug.Error');
goog.require('goog.json');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XmlHttp');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('goog.userAgent');
goog.scope(function() {
var xhr = goog.labs.net.xhr;
var HttpStatus = goog.net.HttpStatus;
/**
* Configuration options for an XMLHttpRequest.
* - headers: map of header key/value pairs.
* - timeoutMs: number of milliseconds after which the request will be timed
* out by the client. Default is to allow the browser to handle timeouts.
* - withCredentials: whether user credentials are to be included in a
* cross-origin request. See:
* http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
* - mimeType: allows the caller to override the content-type and charset for
* the request. See:
* http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
* - responseType: may be set to change the response type to an arraybuffer or
* blob for downloading binary data. See:
* http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype]
* - xmlHttpFactory: allows the caller to override the factory used to create
* XMLHttpRequest objects.
* - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
* be removed before parsing the response as JSON.
*
* @typedef {{
* headers: (Object<string>|undefined),
* mimeType: (string|undefined),
* responseType: (xhr.ResponseType|undefined),
* timeoutMs: (number|undefined),
* withCredentials: (boolean|undefined),
* xmlHttpFactory: (goog.net.XmlHttpFactory|undefined),
* xssiPrefix: (string|undefined)
* }}
*/
xhr.Options;
/**
* Defines the types that are allowed as post data.
* @typedef {(ArrayBuffer|Blob|Document|FormData|null|string|undefined)}
*/
xhr.PostData;
/**
* The Content-Type HTTP header name.
* @type {string}
*/
xhr.CONTENT_TYPE_HEADER = 'Content-Type';
/**
* The Content-Type HTTP header value for a url-encoded form.
* @type {string}
*/
xhr.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';
/**
* Supported data types for the responseType field.
* See: http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-response
* @enum {string}
*/
xhr.ResponseType = {
ARRAYBUFFER: 'arraybuffer',
BLOB: 'blob',
DOCUMENT: 'document',
JSON: 'json',
TEXT: 'text'
};
/**
* Sends a get request, returning a promise that will be resolved
* with the response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<string>} A promise that will be resolved with the
* response text once the request completes.
*/
xhr.get = function(url, opt_options) {
return xhr.send('GET', url, null, opt_options).then(function(request) {
return request.responseText;
});
};
/**
* Sends a post request, returning a promise that will be resolved
* with the response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.PostData} data The body of the post request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<string>} A promise that will be resolved with the
* response text once the request completes.
*/
xhr.post = function(url, data, opt_options) {
return xhr.send('POST', url, data, opt_options).then(function(request) {
return request.responseText;
});
};
/**
* Sends a get request, returning a promise that will be resolved with
* the parsed response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<Object>} A promise that will be resolved with the
* response JSON once the request completes.
*/
xhr.getJson = function(url, opt_options) {
return xhr.send('GET', url, null, opt_options).then(function(request) {
return xhr.parseJson_(request.responseText, opt_options);
});
};
/**
* Sends a get request, returning a promise that will be resolved with the
* response as an array of bytes.
*
* Supported in all XMLHttpRequest level 2 browsers, as well as IE9. IE8 and
* earlier are not supported.
*
* @param {string} url The URL to request.
* @param {xhr.Options=} opt_options Configuration options for the request. The
* responseType will be overwritten to 'arraybuffer' if it was set.
* @return {!goog.Promise<!Uint8Array|!Array<number>>} A promise that will be
* resolved with an array of bytes once the request completes.
*/
xhr.getBytes = function(url, opt_options) {
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
throw new Error('getBytes is not supported in this browser.');
}
var options = opt_options || {};
options.responseType = xhr.ResponseType.ARRAYBUFFER;
return xhr.send('GET', url, null, options).then(function(request) {
// Use the ArrayBuffer response in browsers that support XMLHttpRequest2.
// This covers nearly all modern browsers: http://caniuse.com/xhr2
if (request.response) {
return new Uint8Array(/** @type {!ArrayBuffer} */ (request.response));
}
// Fallback for IE9: the response may be accessed as an array of bytes with
// the non-standard responseBody property, which can only be accessed as a
// VBArray. IE7 and IE8 require significant amounts of VBScript to extract
// the bytes.
// See: http://stackoverflow.com/questions/1919972/
if (goog.global['VBArray']) {
return new goog.global['VBArray'](request['responseBody']).toArray();
}
// Nearly all common browsers are covered by the cases above. If downloading
// binary files in older browsers is necessary, the MDN article "Sending and
// Receiving Binary Data" provides techniques that may work with
// XMLHttpRequest level 1 browsers: http://goo.gl/7lEuGN
throw new xhr.Error(
'getBytes is not supported in this browser.', url, request);
});
};
/**
* Sends a post request, returning a promise that will be resolved with
* the parsed response text once the request completes.
*
* @param {string} url The URL to request.
* @param {xhr.PostData} data The body of the post request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<Object>} A promise that will be resolved with the
* response JSON once the request completes.
*/
xhr.postJson = function(url, data, opt_options) {
return xhr.send('POST', url, data, opt_options).then(function(request) {
return xhr.parseJson_(request.responseText, opt_options);
});
};
/**
* Sends a request, returning a promise that will be resolved
* with the XHR object once the request completes.
*
* If content type hasn't been set in opt_options headers, and hasn't been
* explicitly set to null, default to form-urlencoded/UTF8 for POSTs.
*
* @param {string} method The HTTP method for the request.
* @param {string} url The URL to request.
* @param {xhr.PostData} data The body of the post request.
* @param {xhr.Options=} opt_options Configuration options for the request.
* @return {!goog.Promise<!goog.net.XhrLike.OrNative>} A promise that will be
* resolved with the XHR object once the request completes.
*/
xhr.send = function(method, url, data, opt_options) {
return new goog.Promise(function(resolve, reject) {
var options = opt_options || {};
var timer;
var request = options.xmlHttpFactory ?
options.xmlHttpFactory.createInstance() : goog.net.XmlHttp();
try {
request.open(method, url, true);
} catch (e) {
// XMLHttpRequest.open may throw when 'open' is called, for example, IE7
// throws "Access Denied" for cross-origin requests.
reject(new xhr.Error('Error opening XHR: ' + e.message, url, request));
}
// So sad that IE doesn't support onload and onerror.
request.onreadystatechange = function() {
if (request.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
goog.global.clearTimeout(timer);
// Note: When developing locally, XHRs to file:// schemes return
// a status code of 0. We mark that case as a success too.
if (HttpStatus.isSuccess(request.status) ||
request.status === 0 && !xhr.isEffectiveSchemeHttp_(url)) {
resolve(request);
} else {
reject(new xhr.HttpError(request.status, url, request));
}
}
};
request.onerror = function() {
reject(new xhr.Error('Network error', url, request));
};
// Set the headers.
var contentType;
if (options.headers) {
for (var key in options.headers) {
var value = options.headers[key];
if (goog.isDefAndNotNull(value)) {
request.setRequestHeader(key, value);
}
}
contentType = options.headers[xhr.CONTENT_TYPE_HEADER];
}
// Browsers will automatically set the content type to multipart/form-data
// when passed a FormData object.
var dataIsFormData = (goog.global['FormData'] &&
(data instanceof goog.global['FormData']));
// If a content type hasn't been set, it hasn't been explicitly set to null,
// and the data isn't a FormData, default to form-urlencoded/UTF8 for POSTs.
// This is because some proxies have been known to reject posts without a
// content-type.
if (method == 'POST' && contentType === undefined && !dataIsFormData) {
request.setRequestHeader(xhr.CONTENT_TYPE_HEADER, xhr.FORM_CONTENT_TYPE);
}
// Set whether to include cookies with cross-domain requests. See:
// http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
if (options.withCredentials) {
request.withCredentials = options.withCredentials;
}
// Allows setting an alternative response type, such as an ArrayBuffer. See:
// http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype
if (options.responseType) {
request.responseType = options.responseType;
}
// Allow the request to override the MIME type of the response. See:
// http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
if (options.mimeType) {
request.overrideMimeType(options.mimeType);
}
// Handle timeouts, if requested.
if (options.timeoutMs > 0) {
timer = goog.global.setTimeout(function() {
// Clear event listener before aborting so the errback will not be
// called twice.
request.onreadystatechange = goog.nullFunction;
request.abort();
reject(new xhr.TimeoutError(url, request));
}, options.timeoutMs);
}
// Trigger the send.
try {
request.send(data);
} catch (e) {
// XMLHttpRequest.send is known to throw on some versions of FF,
// for example if a cross-origin request is disallowed.
request.onreadystatechange = goog.nullFunction;
goog.global.clearTimeout(timer);
reject(new xhr.Error('Error sending XHR: ' + e.message, url, request));
}
});
};
/**
* @param {string} url The URL to test.
* @return {boolean} Whether the effective scheme is HTTP or HTTPs.
* @private
*/
xhr.isEffectiveSchemeHttp_ = function(url) {
var scheme = goog.uri.utils.getEffectiveScheme(url);
// NOTE(user): Empty-string is for the case under FF3.5 when the location
// is not defined inside a web worker.
return scheme == 'http' || scheme == 'https' || scheme == '';
};
/**
* JSON-parses the given response text, returning an Object.
*
* @param {string} responseText Response text.
* @param {xhr.Options|undefined} options The options object.
* @return {Object} The JSON-parsed value of the original responseText.
* @private
*/
xhr.parseJson_ = function(responseText, options) {
var prefixStrippedResult = responseText;
if (options && options.xssiPrefix) {
prefixStrippedResult = xhr.stripXssiPrefix_(
options.xssiPrefix, prefixStrippedResult);
}
return goog.json.parse(prefixStrippedResult);
};
/**
* Strips the XSSI prefix from the input string.
*
* @param {string} prefix The XSSI prefix.
* @param {string} string The string to strip the prefix from.
* @return {string} The input string without the prefix.
* @private
*/
xhr.stripXssiPrefix_ = function(prefix, string) {
if (goog.string.startsWith(string, prefix)) {
string = string.substring(prefix.length);
}
return string;
};
/**
* Generic error that may occur during a request.
*
* @param {string} message The error message.
* @param {string} url The URL that was being requested.
* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
* @extends {goog.debug.Error}
* @constructor
*/
xhr.Error = function(message, url, request) {
xhr.Error.base(this, 'constructor', message + ', url=' + url);
/**
* The URL that was requested.
* @type {string}
*/
this.url = url;
/**
* The XMLHttpRequest corresponding with the failed request.
* @type {!goog.net.XhrLike.OrNative}
*/
this.xhr = request;
};
goog.inherits(xhr.Error, goog.debug.Error);
/** @override */
xhr.Error.prototype.name = 'XhrError';
/**
* Class for HTTP errors.
*
* @param {number} status The HTTP status code of the response.
* @param {string} url The URL that was being requested.
* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
* @extends {xhr.Error}
* @constructor
* @final
*/
xhr.HttpError = function(status, url, request) {
xhr.HttpError.base(
this, 'constructor', 'Request Failed, status=' + status, url, request);
/**
* The HTTP status code for the error.
* @type {number}
*/
this.status = status;
};
goog.inherits(xhr.HttpError, xhr.Error);
/** @override */
xhr.HttpError.prototype.name = 'XhrHttpError';
/**
* Class for Timeout errors.
*
* @param {string} url The URL that timed out.
* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
* @extends {xhr.Error}
* @constructor
* @final
*/
xhr.TimeoutError = function(url, request) {
xhr.TimeoutError.base(this, 'constructor', 'Request timed out', url, request);
};
goog.inherits(xhr.TimeoutError, xhr.Error);
/** @override */
xhr.TimeoutError.prototype.name = 'XhrTimeoutError';
}); // goog.scope
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.net.xhr
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.net.xhrTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,462 @@
// Copyright 2011 The Closure Library 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.
goog.provide('goog.labs.net.xhrTest');
goog.setTestOnly('goog.labs.net.xhrTest');
goog.require('goog.Promise');
goog.require('goog.labs.net.xhr');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XmlHttp');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
function stubXhrToReturn(status, opt_responseText, opt_latency) {
if (goog.isDefAndNotNull(opt_latency)) {
mockClock = new goog.testing.MockClock(true);
}
var stubXhr = {
sent: false,
aborted: false,
status: 0,
headers: {},
open: function(method, url, async) {
this.method = method;
this.url = url;
this.async = async;
},
setRequestHeader: function(key, value) {
this.headers[key] = value;
},
overrideMimeType: function(mimeType) {
this.mimeType = mimeType;
},
abort: function() {
this.aborted = true;
this.load(0);
},
send: function(data) {
if (mockClock) {
mockClock.tick(opt_latency);
}
this.data = data;
this.sent = true;
this.load(status);
},
load: function(status) {
this.status = status;
if (goog.isDefAndNotNull(opt_responseText)) {
this.responseText = opt_responseText;
}
this.readyState = 4;
if (this.onreadystatechange) this.onreadystatechange();
}
};
stubXmlHttpWith(stubXhr);
}
function stubXhrToThrow(err) {
stubXmlHttpWith(buildThrowingStubXhr(err));
}
function buildThrowingStubXhr(err) {
return {
sent: false,
aborted: false,
status: 0,
headers: {},
open: function(method, url, async) {
this.method = method;
this.url = url;
this.async = async;
},
setRequestHeader: function(key, value) {
this.headers[key] = value;
},
overrideMimeType: function(mimeType) {
this.mimeType = mimeType;
},
send: function(data) {
throw err;
}
};
}
function stubXmlHttpWith(stubXhr) {
goog.net.XmlHttp = function() {
return stubXhr;
};
for (var x in originalXmlHttp) {
goog.net.XmlHttp[x] = originalXmlHttp[x];
}
}
var xhr = goog.labs.net.xhr;
var originalXmlHttp = goog.net.XmlHttp;
var mockClock;
function tearDown() {
if (mockClock) {
mockClock.dispose();
mockClock = null;
}
goog.net.XmlHttp = originalXmlHttp;
}
/**
* Tests whether the test was loaded from a file: protocol. Tests that use a
* real network request cannot be run from the local file system due to
* cross-origin restrictions, but will run if the tests are hosted on a server.
* A log message is added to the test case to warn users that the a test was
* skipped.
*
* @return {boolean} Whether the test is running on a local file system.
*/
function isRunningLocally() {
if (window.location.protocol == 'file:') {
var testCase = goog.global['G_testRunner'].testCase;
testCase.saveMessage('Test skipped while running on local file system.');
return true;
}
return false;
}
function testSimpleRequest() {
if (isRunningLocally()) return;
return xhr.send('GET', 'testdata/xhr_test_text.data').then(function(xhr) {
assertEquals('Just some data.', xhr.responseText);
assertEquals(200, xhr.status);
});
}
function testGetText() {
if (isRunningLocally()) return;
return xhr.get('testdata/xhr_test_text.data').then(function(responseText) {
assertEquals('Just some data.', responseText);
});
}
function testGetTextWithJson() {
if (isRunningLocally()) return;
return xhr.get('testdata/xhr_test_json.data').then(function(responseText) {
assertEquals('while(1);\n{"stat":"ok","count":12345}\n', responseText);
});
}
function testPostText() {
if (isRunningLocally()) return;
return xhr.post('testdata/xhr_test_text.data', 'post-data').then(
function(responseText) {
// No good way to test post-data gets transported.
assertEquals('Just some data.', responseText);
});
}
function testGetJson() {
if (isRunningLocally()) return;
return xhr.getJson(
'testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'}).then(
function(responseObj) {
assertEquals('ok', responseObj['stat']);
assertEquals(12345, responseObj['count']);
});
}
function testGetBytes() {
if (isRunningLocally()) return;
// IE8 requires a VBScript fallback to read the bytes from the response.
if (goog.userAgent.IE && !goog.userAgent.isDocumentMode(9)) {
return;
}
return xhr.getBytes('testdata/cleardot.gif').then(function(bytes) {
assertElementsEquals([
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0xFF,
0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00,
0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3B
], bytes);
});
}
function testSerialRequests() {
if (isRunningLocally()) return;
return xhr.get('testdata/xhr_test_text.data').
then(function(response) {
return xhr.getJson(
'testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'});
}).then(function(responseObj) {
// Data that comes through to callbacks should be from the 2nd request.
assertEquals('ok', responseObj['stat']);
assertEquals(12345, responseObj['count']);
});
}
function testBadUrlDetectedAsError() {
if (isRunningLocally()) return;
return xhr.getJson('unknown-file.dat').then(
fail /* opt_onFulfilled */,
function(err) {
assertTrue(
'Error should be an HTTP error', err instanceof xhr.HttpError);
assertEquals(404, err.status);
assertNotNull(err.xhr);
});
}
function testBadOriginTriggersOnErrorHandler() {
return xhr.get('http://www.google.com').then(
fail /* opt_onFulfilled */,
function(err) {
// In IE this will be a goog.labs.net.xhr.Error since it is thrown
// when calling xhr.open(), other browsers will raise an HttpError.
assertTrue('Error should be an xhr error', err instanceof xhr.Error);
assertNotNull(err.xhr);
});
}
//============================================================================
// The following tests use a stubbed out XMLHttpRequest.
//============================================================================
function testAbortRequest() {
stubXhrToReturn(200);
var promise = xhr.send('GET', 'test-url', null).thenCatch(
function(error) {
assertTrue(error instanceof goog.Promise.CancellationError);
});
promise.cancel();
return promise;
}
function testSendNoOptions() {
var called = false;
stubXhrToReturn(200);
assertFalse('Callback should not yet have been called', called);
return xhr.send('GET', 'test-url', null).then(function(stubXhr) {
called = true;
assertEquals('GET', stubXhr.method);
assertEquals('test-url', stubXhr.url);
});
}
function testSendPostSetsDefaultHeader() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals('application/x-www-form-urlencoded;charset=utf-8',
stubXhr.headers['Content-Type']);
});
}
function testSendPostDoesntSetHeaderWithFormData() {
if (!goog.global['FormData']) { return; }
var formData = new goog.global['FormData']();
formData.append('name', 'value');
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', formData).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals(undefined, stubXhr.headers['Content-Type']);
});
}
function testSendPostHeaders() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null,
{ headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'} }).
then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals('text/plain', stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
});
}
function testSendPostHeadersWithFormData() {
if (!goog.global['FormData']) { return; }
var formData = new goog.global['FormData']();
formData.append('name', 'value');
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', formData,
{ headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'} }).
then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals('text/plain', stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
});
}
function testSendNullPostHeaders() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null, {
headers: {
'Content-Type': null,
'X-Made-Up': 'FooBar',
'Y-Made-Up': null
}
}).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals(undefined, stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
assertEquals(undefined, stubXhr.headers['Y-Made-Up']);
});
}
function testSendNullPostHeadersWithFormData() {
if (!goog.global['FormData']) { return; }
var formData = new goog.global['FormData']();
formData.append('name', 'value');
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', formData, {
headers: {
'Content-Type': null,
'X-Made-Up': 'FooBar',
'Y-Made-Up': null
}
}).then(function(stubXhr) {
assertEquals('POST', stubXhr.method);
assertEquals('test-url', stubXhr.url);
assertEquals(undefined, stubXhr.headers['Content-Type']);
assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
assertEquals(undefined, stubXhr.headers['Y-Made-Up']);
});
}
function testSendWithCredentials() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null, {withCredentials: true}).
then(function(stubXhr) {
assertTrue('XHR should have been sent', stubXhr.sent);
assertTrue(stubXhr.withCredentials);
});
}
function testSendWithMimeType() {
stubXhrToReturn(200);
return xhr.send('POST', 'test-url', null, {mimeType: 'text/plain'}).
then(function(stubXhr) {
assertTrue('XHR should have been sent', stubXhr.sent);
assertEquals('text/plain', stubXhr.mimeType);
});
}
function testSendWithHttpError() {
stubXhrToReturn(500);
return xhr.send('POST', 'test-url', null).then(
fail /* opt_onResolved */,
function(err) {
assertTrue(err instanceof xhr.HttpError);
assertTrue(err.xhr.sent);
assertEquals(500, err.status);
});
}
function testSendWithTimeoutNotHit() {
stubXhrToReturn(200, null /* opt_responseText */, 1400 /* opt_latency */);
return xhr.send('POST', 'test-url', null, {timeoutMs: 1500}).
then(function(stubXhr) {
assertTrue(mockClock.getTimeoutsMade() > 0);
assertTrue('XHR should have been sent', stubXhr.sent);
assertFalse('XHR should not have been aborted', stubXhr.aborted);
});
}
function testSendWithTimeoutHit() {
stubXhrToReturn(200, null /* opt_responseText */, 50 /* opt_latency */);
return xhr.send('POST', 'test-url', null, {timeoutMs: 50}).then(
fail /* opt_onResolved */,
function(err) {
assertTrue('XHR should have been sent', err.xhr.sent);
assertTrue('XHR should have been aborted', err.xhr.aborted);
assertTrue(err instanceof xhr.TimeoutError);
});
}
function testCancelRequest() {
stubXhrToReturn(200, null /* opt_responseText */, 25);
var promise = xhr.send('GET', 'test-url', null, {timeoutMs: 50});
promise.then(
fail /* opt_onResolved */,
function(error) {
assertTrue('XHR should have been sent', error.xhr.sent);
if (error instanceof goog.Promise.CancellationError) {
error.xhr.abort();
}
assertTrue('XHR should have been aborted', error.xhr.aborted);
assertTrue(error instanceof goog.Promise.CancellationError);
});
promise.cancel();
return promise;
}
function testGetJson() {
var stubXhr = stubXhrToReturn(200, '{"a": 1, "b": 2}');
xhr.getJson('test-url').then(function(responseObj) {
assertObjectEquals({a: 1, b: 2}, responseObj);
});
}
function testGetJsonWithXssiPrefix() {
stubXhrToReturn(200, 'while(1);\n{"a": 1, "b": 2}');
return xhr.getJson('test-url', {xssiPrefix: 'while(1);\n'}).then(
function(responseObj) {
assertObjectEquals({a: 1, b: 2}, responseObj);
});
}
function testSendWithClientException() {
stubXhrToThrow(new Error('CORS XHR with file:// schemas not allowed.'));
return xhr.send('POST', 'file://test-url', null).then(
fail /* opt_onResolved */,
function(err) {
assertFalse('XHR should not have been sent', err.xhr.sent);
assertTrue(err instanceof Error);
assertTrue(
/CORS XHR with file:\/\/ schemas not allowed./.test(err.message));
});
}
function testSendWithFactory() {
stubXhrToReturn(200);
var options = {
xmlHttpFactory: new goog.net.WrapperXmlHttpFactory(
goog.partial(buildThrowingStubXhr, new Error('Bad factory')),
goog.net.XmlHttp.getOptions)
};
return xhr.send('POST', 'file://test-url', null, options).then(
fail /* opt_onResolved */,
function(err) {
assertTrue(err instanceof Error);
});
}
@@ -0,0 +1,47 @@
// Copyright 2012 The Closure Library 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 A labs location for functions destined for Closure's
* {@code goog.object} namespace.
* @author chrishenry@google.com (Chris Henry)
*/
goog.provide('goog.labs.object');
/**
* Whether two values are not observably distinguishable. This
* correctly detects that 0 is not the same as -0 and two NaNs are
* practically equivalent.
*
* The implementation is as suggested by harmony:egal proposal.
*
* @param {*} v The first value to compare.
* @param {*} v2 The second value to compare.
* @return {boolean} Whether two values are not observably distinguishable.
* @see http://wiki.ecmascript.org/doku.php?id=harmony:egal
*/
goog.labs.object.is = function(v, v2) {
if (v === v2) {
// 0 === -0, but they are not identical.
// We need the cast because the compiler requires that v2 is a
// number (although 1/v2 works with non-number). We cast to ? to
// stop the compiler from type-checking this statement.
return v !== 0 || 1 / v === 1 / /** @type {?} */ (v2);
}
// NaN is non-reflexive: NaN !== NaN, although they are identical.
return v !== v && v2 !== v2;
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: chrishenry@google.com (Chris Henry)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.object
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.objectTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,52 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.objectTest');
goog.setTestOnly('goog.labs.objectTest');
goog.require('goog.labs.object');
goog.require('goog.testing.jsunit');
function testIs() {
var object = {};
assertTrue(goog.labs.object.is(object, object));
assertFalse(goog.labs.object.is(object, {}));
assertTrue(goog.labs.object.is(NaN, NaN));
assertTrue(goog.labs.object.is(0, 0));
assertTrue(goog.labs.object.is(1, 1));
assertTrue(goog.labs.object.is(-1, -1));
assertTrue(goog.labs.object.is(123, 123));
assertFalse(goog.labs.object.is(0, -0));
assertFalse(goog.labs.object.is(-0, 0));
assertFalse(goog.labs.object.is(0, 1));
assertTrue(goog.labs.object.is(true, true));
assertTrue(goog.labs.object.is(false, false));
assertFalse(goog.labs.object.is(true, false));
assertFalse(goog.labs.object.is(false, true));
assertTrue(goog.labs.object.is('', ''));
assertTrue(goog.labs.object.is('a', 'a'));
assertFalse(goog.labs.object.is('', 'a'));
assertFalse(goog.labs.object.is('a', ''));
assertFalse(goog.labs.object.is('a', 'b'));
assertFalse(goog.labs.object.is(true, 'true'));
assertFalse(goog.labs.object.is('true', true));
assertFalse(goog.labs.object.is(false, 'false'));
assertFalse(goog.labs.object.is('false', false));
assertFalse(goog.labs.object.is(0, '0'));
assertFalse(goog.labs.object.is('0', 0));
}
@@ -0,0 +1,564 @@
// Copyright 2014 The Closure Library 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.
goog.provide('goog.labs.pubsub.BroadcastPubSub');
goog.require('goog.Disposable');
goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.async.run');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.json');
goog.require('goog.log');
goog.require('goog.math');
goog.require('goog.pubsub.PubSub');
goog.require('goog.storage.Storage');
goog.require('goog.storage.mechanism.HTML5LocalStorage');
goog.require('goog.string');
goog.require('goog.userAgent');
/**
* Topic-based publish/subscribe messaging implementation that provides
* communication between browsing contexts that share the same origin.
*
* Wrapper around PubSub that utilizes localStorage to broadcast publications to
* all browser windows with the same origin as the publishing context. This
* allows for topic-based publish/subscribe implementation of strings shared by
* all browser contexts that share the same origin.
*
* Delivery is guaranteed on all browsers except IE8 where topics expire after a
* timeout. Publishing of a topic within a callback function provides no
* guarantee on ordering in that there is a possiblilty that separate origin
* contexts may see topics in a different order.
*
* This class is not secure and in certain cases (e.g., a browser crash) data
* that is published can persist in localStorage indefinitely. Do not use this
* class to communicate private or confidential information.
*
* On IE8, localStorage is shared by the http and https origins. An attacker
* could possibly leverage this to publish to the secure origin.
*
* goog.labs.pubsub.BroadcastPubSub wraps an instance of PubSub rather than
* subclassing because the base PubSub class allows publishing of arbitrary
* objects.
*
* Special handling is done for the IE8 browsers. See the IE8_EVENTS_KEY_
* constant and the {@code publish} function for more information.
*
*
* @constructor @struct @extends {goog.Disposable}
* @suppress {checkStructDictInheritance}
*/
goog.labs.pubsub.BroadcastPubSub = function() {
goog.labs.pubsub.BroadcastPubSub.base(this, 'constructor');
goog.labs.pubsub.BroadcastPubSub.instances_.push(this);
/** @private @const */
this.pubSub_ = new goog.pubsub.PubSub();
this.registerDisposable(this.pubSub_);
/** @private @const */
this.handler_ = new goog.events.EventHandler(this);
this.registerDisposable(this.handler_);
/** @private @const */
this.logger_ = goog.log.getLogger('goog.labs.pubsub.BroadcastPubSub');
/** @private @const */
this.mechanism_ = new goog.storage.mechanism.HTML5LocalStorage();
/** @private {goog.storage.Storage} */
this.storage_ = null;
/** @private {Object<string, number>} */
this.ie8LastEventTimes_ = null;
/** @private {number} */
this.ie8StartupTimestamp_ = goog.now() - 1;
if (this.mechanism_.isAvailable()) {
this.storage_ = new goog.storage.Storage(this.mechanism_);
var target = window;
if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {
this.ie8LastEventTimes_ = {};
target = document;
}
this.handler_.listen(target,
goog.events.EventType.STORAGE,
this.handleStorageEvent_);
}
};
goog.inherits(goog.labs.pubsub.BroadcastPubSub, goog.Disposable);
/** @private @const {!Array<!goog.labs.pubsub.BroadcastPubSub>} */
goog.labs.pubsub.BroadcastPubSub.instances_ = [];
/**
* SitePubSub namespace for localStorage.
* @private @const
*/
goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_ = '_closure_bps';
/**
* Handle the storage event and possibly dispatch topics.
* @param {!goog.events.Event} e Event object.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.prototype.handleStorageEvent_ =
function(e) {
if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {
// Even though we have the event, IE8 doesn't update our localStorage until
// after we handle the actual event.
goog.async.run(this.handleIe8StorageEvent_, this);
return;
}
var browserEvent = e.getBrowserEvent();
if (browserEvent.key !=
goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_) {
return;
}
var data = goog.json.parse(browserEvent.newValue);
var args = goog.isObject(data) && data['args'];
if (goog.isArray(args) && goog.array.every(args, goog.isString)) {
this.dispatch_(args);
} else {
goog.log.warning(this.logger_, 'storage event contained invalid arguments');
}
};
/**
* Dispatches args on the internal pubsub queue.
* @param {!Array<string>} args The arguments to publish.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.prototype.dispatch_ = function(args) {
goog.pubsub.PubSub.prototype.publish.apply(this.pubSub_, args);
};
/**
* Publishes a message to a topic. Remote subscriptions in other tabs/windows
* are dispatched via local storage events. Local subscriptions are called
* asynchronously via Timer event in order to simulate remote behavior locally.
* @param {string} topic Topic to publish to.
* @param {...string} var_args String arguments that are applied to each
* subscription function.
*/
goog.labs.pubsub.BroadcastPubSub.prototype.publish =
function(topic, var_args) {
var args = goog.array.toArray(arguments);
// Dispatch to localStorage.
if (this.storage_) {
// Update topics to use the optional prefix.
var now = goog.now();
var data = {
'args': args,
'timestamp': now
};
if (!goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {
// Generated events will contain all the data in modern browsers.
this.storage_.set(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_, data);
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_);
} else {
// With IE8 we need to manage our own events queue.
var events = null;
/** @preserveTry */
try {
events = this.storage_.get(
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
} catch (ex) {
goog.log.error(this.logger_,
'publish encountered invalid event queue at ' +
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
}
if (!goog.isArray(events)) {
events = [];
}
// Avoid a race condition where we're publishing in the same
// millisecond that another event that may be getting
// processed. In short, we try go guarantee that whatever event
// we put on the event queue has a timestamp that is older than
// any other timestamp in the queue.
var lastEvent = events[events.length - 1];
var lastTimestamp = lastEvent && lastEvent['timestamp'] ||
this.ie8StartupTimestamp_;
if (lastTimestamp >= now) {
now = lastTimestamp +
goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_;
data['timestamp'] = now;
}
events.push(data);
this.storage_.set(
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_, events);
// Cleanup this event in IE8_EVENT_LIFETIME_MS_ milliseconds.
goog.Timer.callOnce(goog.bind(this.cleanupIe8StorageEvents_, this, now),
goog.labs.pubsub.BroadcastPubSub.IE8_EVENT_LIFETIME_MS_);
}
}
// W3C spec is to not dispatch the storage event to the same window that
// modified localStorage. For conforming browsers we have to manually dispatch
// the publish event to subscriptions on instances of BroadcastPubSub in the
// current window.
if (!goog.userAgent.IE) {
// Dispatch the publish event to local instances asynchronously to fix some
// quirks with timings. The result is that all subscriptions are dispatched
// before any future publishes are processed. The effect is that
// subscriptions in the same window are dispatched as if they are the result
// of a publish from another tab.
goog.array.forEach(goog.labs.pubsub.BroadcastPubSub.instances_,
function(instance) {
goog.async.run(goog.bind(instance.dispatch_, instance, args));
});
}
};
/**
* Unsubscribes a function from a topic. Only deletes the first match found.
* Returns a Boolean indicating whether a subscription was removed.
* @param {string} topic Topic to unsubscribe from.
* @param {Function} fn Function to unsubscribe.
* @param {Object=} opt_context Object in whose context the function was to be
* called (the global scope if none).
* @return {boolean} Whether a matching subscription was removed.
*/
goog.labs.pubsub.BroadcastPubSub.prototype.unsubscribe =
function(topic, fn, opt_context) {
return this.pubSub_.unsubscribe(topic, fn, opt_context);
};
/**
* Removes a subscription based on the key returned by {@link #subscribe}. No-op
* if no matching subscription is found. Returns a Boolean indicating whether a
* subscription was removed.
* @param {number} key Subscription key.
* @return {boolean} Whether a matching subscription was removed.
*/
goog.labs.pubsub.BroadcastPubSub.prototype.unsubscribeByKey = function(key) {
return this.pubSub_.unsubscribeByKey(key);
};
/**
* Subscribes a function to a topic. The function is invoked as a method on the
* given {@code opt_context} object, or in the global scope if no context is
* specified. Subscribing the same function to the same topic multiple times
* will result in multiple function invocations while publishing. Returns a
* subscription key that can be used to unsubscribe the function from the topic
* via {@link #unsubscribeByKey}.
* @param {string} topic Topic to subscribe to.
* @param {Function} fn Function to be invoked when a message is published to
* the given topic.
* @param {Object=} opt_context Object in whose context the function is to be
* called (the global scope if none).
* @return {number} Subscription key.
*/
goog.labs.pubsub.BroadcastPubSub.prototype.subscribe =
function(topic, fn, opt_context) {
return this.pubSub_.subscribe(topic, fn, opt_context);
};
/**
* Subscribes a single-use function to a topic. The function is invoked as a
* method on the given {@code opt_context} object, or in the global scope if no
* context is specified, and is then unsubscribed. Returns a subscription key
* that can be used to unsubscribe the function from the topic via {@link
* #unsubscribeByKey}.
* @param {string} topic Topic to subscribe to.
* @param {Function} fn Function to be invoked once and then unsubscribed when
* a message is published to the given topic.
* @param {Object=} opt_context Object in whose context the function is to be
* called (the global scope if none).
* @return {number} Subscription key.
*/
goog.labs.pubsub.BroadcastPubSub.prototype.subscribeOnce =
function(topic, fn, opt_context) {
return this.pubSub_.subscribeOnce(topic, fn, opt_context);
};
/**
* Returns the number of subscriptions to the given topic (or all topics if
* unspecified).
* @param {string=} opt_topic The topic (all topics if unspecified).
* @return {number} Number of subscriptions to the topic.
*/
goog.labs.pubsub.BroadcastPubSub.prototype.getCount = function(opt_topic) {
return this.pubSub_.getCount(opt_topic);
};
/**
* Clears the subscription list for a topic, or all topics if unspecified.
* @param {string=} opt_topic Topic to clear (all topics if unspecified).
*/
goog.labs.pubsub.BroadcastPubSub.prototype.clear = function(opt_topic) {
this.pubSub_.clear(opt_topic);
};
/** @override */
goog.labs.pubsub.BroadcastPubSub.prototype.disposeInternal = function() {
goog.array.remove(goog.labs.pubsub.BroadcastPubSub.instances_, this);
if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_ &&
goog.isDefAndNotNull(this.storage_) &&
goog.labs.pubsub.BroadcastPubSub.instances_.length == 0) {
this.storage_.remove(
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
}
goog.labs.pubsub.BroadcastPubSub.base(this, 'disposeInternal');
};
/**
* Prefix for IE8 storage event queue keys.
* @private @const
*/
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ = '_closure_bps_ie8evt';
/**
* Time (in milliseconds) that IE8 events should live. If they are not
* processed by other windows in this time they will be removed.
* @private @const
*/
goog.labs.pubsub.BroadcastPubSub.IE8_EVENT_LIFETIME_MS_ = 1000 * 10;
/**
* Time (in milliseconds) that the IE8 event queue should live.
* @private @const
*/
goog.labs.pubsub.BroadcastPubSub.IE8_QUEUE_LIFETIME_MS_ = 1000 * 30;
/**
* Time delta that is used to distinguish between timestamps of events that
* happen in the same millisecond.
* @private @const
*/
goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_ = .01;
/**
* Name for this window/tab's storage key that stores its IE8 event queue.
*
* The browsers storage events are supposed to track the key which was changed,
* the previous value for that key, and the new value of that key. Our
* implementation is dependent on this information but IE8 doesn't provide it.
* We implement our own event queue using local storage to track this
* information in IE8. Since all instances share the same localStorage context
* in a particular tab, we share the events queue.
*
* This key is a static member shared by all instances of BroadcastPubSub in the
* same Window context. To avoid read-update-write contention, this key is only
* written in a single context in the cleanupIe8StorageEvents_ function. Since
* instances in other contexts will read this key there is code in the {@code
* publish} function to make sure timestamps are unique even within the same
* millisecond.
*
* @private @const
*/
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_ =
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ +
goog.math.randomInt(1e9);
/**
* All instances of this object should access elements using strings and not
* attributes. Since we are communicating across browser tabs we could be
* dealing with different versions of javascript and thus may have different
* obfuscation in each tab.
* @private @typedef {{'timestamp': number, 'args': !Array<string>}}
*/
goog.labs.pubsub.BroadcastPubSub.Ie8Event_;
/** @private @const */
goog.labs.pubsub.BroadcastPubSub.IS_IE8_ =
goog.userAgent.IE && goog.userAgent.DOCUMENT_MODE == 8;
/**
* Validates an event object.
* @param {!Object} obj The object to validate as an Event.
* @return {?goog.labs.pubsub.BroadcastPubSub.Ie8Event_} A valid
* event object or null if the object is invalid.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.validateIe8Event_ = function(obj) {
if (goog.isObject(obj) && goog.isNumber(obj['timestamp']) &&
goog.array.every(obj['args'], goog.isString)) {
return {'timestamp': obj['timestamp'], 'args': obj['args']};
}
return null;
};
/**
* Returns an array of valid IE8 events.
* @param {!Array<!Object>} events Possible IE8 events.
* @return {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>}
* Valid IE8 events.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_ = function(events) {
return goog.array.filter(goog.array.map(events,
goog.labs.pubsub.BroadcastPubSub.validateIe8Event_),
goog.isDefAndNotNull);
};
/**
* Returns the IE8 events that have a timestamp later than the provided
* timestamp.
* @param {number} timestamp Expired timestamp.
* @param {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>} events
* Possible IE8 events.
* @return {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>}
* Unexpired IE8 events.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_ =
function(timestamp, events) {
return goog.array.filter(events, function(event) {
return event['timestamp'] > timestamp;
});
};
/**
* Processes the events array for key if all elements are valid IE8 events.
* @param {string} key The key in localStorage where the event queue is stored.
* @param {!Array<!Object>} events Array of possible events stored at key.
* @return {boolean} Return true if all elements in the array are valid
* events, false otherwise.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.prototype.maybeProcessIe8Events_ =
function(key, events) {
if (!events.length) {
return false;
}
var validEvents =
goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_(events);
if (validEvents.length == events.length) {
var lastTimestamp = goog.array.peek(validEvents)['timestamp'];
var previousTime =
this.ie8LastEventTimes_[key] || this.ie8StartupTimestamp_;
if (lastTimestamp > previousTime -
goog.labs.pubsub.BroadcastPubSub.IE8_QUEUE_LIFETIME_MS_) {
this.ie8LastEventTimes_[key] = lastTimestamp;
validEvents = goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_(
previousTime, validEvents);
for (var i = 0, event; event = validEvents[i]; i++) {
this.dispatch_(event['args']);
}
return true;
}
} else {
goog.log.warning(this.logger_, 'invalid events found in queue ' + key);
}
return false;
};
/**
* Handle the storage event and possibly dispatch events. Looks through all keys
* in localStorage for valid keys.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.prototype.handleIe8StorageEvent_ = function() {
var numKeys = this.mechanism_.getCount();
for (var idx = 0; idx < numKeys; idx++) {
var key = this.mechanism_.key(idx);
// Don't process events we generated. The W3C standard says that storage
// events should be queued by the browser for each window whose document's
// storage object is affected by a change in localStorage. Chrome, Firefox,
// and modern IE don't dispatch the event to the window which made the
// change. This code simulates that behavior in IE8.
if (!(goog.isString(key) && goog.string.startsWith(
key, goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_))) {
continue;
}
var events = null;
/** @preserveTry */
try {
events = this.storage_.get(key);
} catch (ex) {
goog.log.warning(this.logger_, 'invalid remote event queue ' + key);
}
if (!(goog.isArray(events) && this.maybeProcessIe8Events_(key, events))) {
// Events is not an array, empty, contains invalid events, or expired.
this.storage_.remove(key);
}
}
};
/**
* Cleanup our IE8 event queue by removing any events that come at or before the
* given timestamp.
* @param {number} timestamp Maximum timestamp to remove from the queue.
* @private
*/
goog.labs.pubsub.BroadcastPubSub.prototype.cleanupIe8StorageEvents_ =
function(timestamp) {
var events = null;
/** @preserveTry */
try {
events = this.storage_.get(
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
} catch (ex) {
goog.log.error(this.logger_,
'cleanup encountered invalid event queue key ' +
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
}
if (!goog.isArray(events)) {
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
return;
}
events = goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_(
timestamp, goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_(
events));
if (events.length > 0) {
this.storage_.set(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_, events);
} else {
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
}
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,284 @@
// Copyright 2013 The Closure Library 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 Provides a convenient API for data persistence with data
* expiration and number of items limit.
*
* Setting and removing values keeps a max number of items invariant.
* Collecting values can be user initiated. If oversize, first removes
* expired items, if still oversize than removes the oldest items until a size
* constraint is fulfilled.
*
*/
goog.provide('goog.labs.storage.BoundedCollectableStorage');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.iter');
goog.require('goog.storage.CollectableStorage');
goog.require('goog.storage.ErrorCode');
goog.require('goog.storage.ExpiringStorage');
goog.scope(function() {
var storage = goog.labs.storage;
/**
* Provides a storage with bounded number of elements, expiring keys and
* a collection method.
*
* @param {!goog.storage.mechanism.IterableMechanism} mechanism The underlying
* storage mechanism.
* @param {number} maxItems Maximum number of items in storage.
* @constructor
* @extends {goog.storage.CollectableStorage}
* @final
*/
storage.BoundedCollectableStorage = function(mechanism, maxItems) {
storage.BoundedCollectableStorage.base(this, 'constructor', mechanism);
/**
* A maximum number of items that should be stored.
* @private {number}
*/
this.maxItems_ = maxItems;
};
goog.inherits(storage.BoundedCollectableStorage,
goog.storage.CollectableStorage);
/**
* An item key used to store a list of keys.
* @const
* @private
*/
storage.BoundedCollectableStorage.KEY_LIST_KEY_ = 'bounded-collectable-storage';
/**
* Recreates a list of keys in order of creation.
*
* @return {!Array<string>} a list of unexpired keys.
* @private
*/
storage.BoundedCollectableStorage.prototype.rebuildIndex_ = function() {
var keys = [];
goog.iter.forEach(this.mechanism.__iterator__(true), function(key) {
if (storage.BoundedCollectableStorage.KEY_LIST_KEY_ == key) {
return;
}
var wrapper;
/** @preserveTry */
try {
wrapper = this.getWrapper(key, true);
} catch (ex) {
if (ex == goog.storage.ErrorCode.INVALID_VALUE) {
// Skip over bad wrappers and continue.
return;
}
// Unknown error, escalate.
throw ex;
}
goog.asserts.assert(wrapper);
var creationTime = goog.storage.ExpiringStorage.getCreationTime(wrapper);
keys.push({key: key, created: creationTime});
}, this);
goog.array.sort(keys, function(a, b) {
return a.created - b.created;
});
return goog.array.map(keys, function(v) {
return v.key;
});
};
/**
* Gets key list from a local storage. If an item does not exist,
* may recreate it.
*
* @param {boolean} rebuild Whether to rebuild a index if no index item exists.
* @return {!Array<string>} a list of keys if index exist, otherwise undefined.
* @private
*/
storage.BoundedCollectableStorage.prototype.getKeys_ = function(rebuild) {
var keys = storage.BoundedCollectableStorage.superClass_.get.call(this,
storage.BoundedCollectableStorage.KEY_LIST_KEY_) || null;
if (!keys || !goog.isArray(keys)) {
if (rebuild) {
keys = this.rebuildIndex_();
} else {
keys = [];
}
}
return /** @type {!Array<string>} */ (keys);
};
/**
* Saves a list of keys in a local storage.
*
* @param {Array<string>} keys a list of keys to save.
* @private
*/
storage.BoundedCollectableStorage.prototype.setKeys_ = function(keys) {
storage.BoundedCollectableStorage.superClass_.set.call(this,
storage.BoundedCollectableStorage.KEY_LIST_KEY_, keys);
};
/**
* Remove subsequence from a sequence.
*
* @param {!Array<string>} keys is a sequence.
* @param {!Array<string>} keysToRemove subsequence of keys, the order must
* be kept.
* @return {!Array<string>} a keys sequence after removing keysToRemove.
* @private
*/
storage.BoundedCollectableStorage.removeSubsequence_ =
function(keys, keysToRemove) {
if (keysToRemove.length == 0) {
return goog.array.clone(keys);
}
var keysToKeep = [];
var keysIdx = 0;
var keysToRemoveIdx = 0;
while (keysToRemoveIdx < keysToRemove.length && keysIdx < keys.length) {
var key = keysToRemove[keysToRemoveIdx];
while (keysIdx < keys.length && keys[keysIdx] != key) {
keysToKeep.push(keys[keysIdx]);
++keysIdx;
}
++keysToRemoveIdx;
}
goog.asserts.assert(keysToRemoveIdx == keysToRemove.length);
goog.asserts.assert(keysIdx < keys.length);
return goog.array.concat(keysToKeep, goog.array.slice(keys, keysIdx + 1));
};
/**
* Keeps the number of items in storage under maxItems. Removes elements in the
* order of creation.
*
* @param {!Array<string>} keys a list of keys in order of creation.
* @param {number} maxSize a number of items to keep.
* @return {!Array<string>} keys left after removing oversize data.
* @private
*/
storage.BoundedCollectableStorage.prototype.collectOversize_ =
function(keys, maxSize) {
if (keys.length <= maxSize) {
return goog.array.clone(keys);
}
var keysToRemove = goog.array.slice(keys, 0, keys.length - maxSize);
goog.array.forEach(keysToRemove, function(key) {
storage.BoundedCollectableStorage.superClass_.remove.call(this, key);
}, this);
return storage.BoundedCollectableStorage.removeSubsequence_(
keys, keysToRemove);
};
/**
* Cleans up the storage by removing expired keys.
*
* @param {boolean=} opt_strict Also remove invalid keys.
* @override
*/
storage.BoundedCollectableStorage.prototype.collect =
function(opt_strict) {
var keys = this.getKeys_(true);
var keysToRemove = this.collectInternal(keys, opt_strict);
keys = storage.BoundedCollectableStorage.removeSubsequence_(
keys, keysToRemove);
this.setKeys_(keys);
};
/**
* Ensures that we keep only maxItems number of items in a local storage.
* @param {boolean=} opt_skipExpired skip removing expired items first.
* @param {boolean=} opt_strict Also remove invalid keys.
*/
storage.BoundedCollectableStorage.prototype.collectOversize =
function(opt_skipExpired, opt_strict) {
var keys = this.getKeys_(true);
if (!opt_skipExpired) {
var keysToRemove = this.collectInternal(keys, opt_strict);
keys = storage.BoundedCollectableStorage.removeSubsequence_(
keys, keysToRemove);
}
keys = this.collectOversize_(keys, this.maxItems_);
this.setKeys_(keys);
};
/**
* Set an item in the storage.
*
* @param {string} key The key to set.
* @param {*} value The value to serialize to a string and save.
* @param {number=} opt_expiration The number of miliseconds since epoch
* (as in goog.now()) when the value is to expire. If the expiration
* time is not provided, the value will persist as long as possible.
* @override
*/
storage.BoundedCollectableStorage.prototype.set =
function(key, value, opt_expiration) {
storage.BoundedCollectableStorage.base(
this, 'set', key, value, opt_expiration);
var keys = this.getKeys_(true);
goog.array.remove(keys, key);
if (goog.isDef(value)) {
keys.push(key);
if (keys.length >= this.maxItems_) {
var keysToRemove = this.collectInternal(keys);
keys = storage.BoundedCollectableStorage.removeSubsequence_(
keys, keysToRemove);
keys = this.collectOversize_(keys, this.maxItems_);
}
}
this.setKeys_(keys);
};
/**
* Remove an item from the data storage.
*
* @param {string} key The key to remove.
* @override
*/
storage.BoundedCollectableStorage.prototype.remove = function(key) {
storage.BoundedCollectableStorage.base(this, 'remove', key);
var keys = this.getKeys_(false);
if (goog.isDef(keys)) {
goog.array.remove(keys, key);
this.setKeys_(keys);
}
};
}); // goog.scope
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.storage.BoundedCollectableStorage
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.storage.BoundedCollectableStorageTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,74 @@
// Copyright 2013 The Closure Library 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.
goog.provide('goog.labs.storage.BoundedCollectableStorageTest');
goog.setTestOnly('goog.labs.storage.BoundedCollectableStorageTest');
goog.require('goog.labs.storage.BoundedCollectableStorage');
goog.require('goog.storage.collectableStorageTester');
goog.require('goog.storage.storage_test');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.storage.FakeMechanism');
function testBasicOperations() {
var mechanism = new goog.testing.storage.FakeMechanism();
var storage = new goog.labs.storage.BoundedCollectableStorage(mechanism, 5);
goog.storage.storage_test.runBasicTests(storage);
}
function testExpiredKeyCollection() {
var mechanism = new goog.testing.storage.FakeMechanism();
var clock = new goog.testing.MockClock(true);
var storage = new goog.labs.storage.BoundedCollectableStorage(mechanism, 15);
goog.storage.collectableStorageTester.runBasicTests(mechanism, clock,
storage);
}
function testLimitingNumberOfItems() {
var mechanism = new goog.testing.storage.FakeMechanism();
var clock = new goog.testing.MockClock(true);
var storage = new goog.labs.storage.BoundedCollectableStorage(mechanism, 2);
// First item should fit.
storage.set('item-1', 'one', 10000);
clock.tick(100);
assertEquals('one', storage.get('item-1'));
// Second item should fit.
storage.set('item-2', 'two', 10000);
assertEquals('one', storage.get('item-1'));
assertEquals('two', storage.get('item-2'));
// Third item is too much, 'item-1' should be removed.
storage.set('item-3', 'three', 5000);
clock.tick(100);
assertUndefined(storage.get('item-1'));
assertEquals('two', storage.get('item-2'));
assertEquals('three', storage.get('item-3'));
clock.tick(5000);
// 'item-3' item has expired, should be removed instead an older 'item-2'.
storage.set('item-4', 'four', 10000);
assertUndefined(storage.get('item-1'));
assertUndefined(storage.get('item-3'));
assertEquals('two', storage.get('item-2'));
assertEquals('four', storage.get('item-4'));
storage.remove('item-2');
storage.remove('item-4');
clock.uninstall();
}
@@ -0,0 +1,348 @@
// Copyright 2012 The Closure Library 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 A map data structure that offers a convenient API to
* manipulate a key, value map. The key must be a string.
*
* This implementation also ensure that you can use keys that would
* not be usable using a normal object literal {}. Some examples
* include __proto__ (all newer browsers), toString/hasOwnProperty (IE
* <= 8).
* @author chrishenry@google.com (Chris Henry)
*/
goog.provide('goog.labs.structs.Map');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.labs.object');
goog.require('goog.object');
/**
* Creates a new map.
* @constructor
* @struct
* @final
*/
goog.labs.structs.Map = function() {
// clear() initializes the map to the empty state.
this.clear();
};
/**
* @type {function(this: Object, string): boolean}
* @private
*/
goog.labs.structs.Map.objectPropertyIsEnumerable_ =
Object.prototype.propertyIsEnumerable;
/**
* @type {function(this: Object, string): boolean}
* @private
*/
goog.labs.structs.Map.objectHasOwnProperty_ =
Object.prototype.hasOwnProperty;
/**
* Primary backing store of this map.
* @type {!Object}
* @private
*/
goog.labs.structs.Map.prototype.map_;
/**
* Secondary backing store for keys. The index corresponds to the
* index for secondaryStoreValues_.
* @type {!Array<string>}
* @private
*/
goog.labs.structs.Map.prototype.secondaryStoreKeys_;
/**
* Secondary backing store for keys. The index corresponds to the
* index for secondaryStoreValues_.
* @type {!Array<*>}
* @private
*/
goog.labs.structs.Map.prototype.secondaryStoreValues_;
/**
* @private {number}
*/
goog.labs.structs.Map.prototype.count_;
/**
* Adds the (key, value) pair, overriding previous entry with the same
* key, if any.
* @param {string} key The key.
* @param {*} value The value.
*/
goog.labs.structs.Map.prototype.set = function(key, value) {
this.assertKeyIsString_(key);
var newKey = !this.hasKeyInPrimaryStore_(key);
this.map_[key] = value;
// __proto__ is not settable on object.
if (key == '__proto__' ||
// Shadows for built-in properties are not enumerable in IE <= 8 .
(!goog.labs.structs.Map.BrowserFeature.OBJECT_CREATE_SUPPORTED &&
!goog.labs.structs.Map.objectPropertyIsEnumerable_.call(
this.map_, key))) {
delete this.map_[key];
var index = goog.array.indexOf(this.secondaryStoreKeys_, key);
if ((newKey = index < 0)) {
index = this.secondaryStoreKeys_.length;
}
this.secondaryStoreKeys_[index] = key;
this.secondaryStoreValues_[index] = value;
}
if (newKey) this.count_++;
};
/**
* Gets the value for the given key.
* @param {string} key The key whose value we want to retrieve.
* @param {*=} opt_default The default value to return if the key does
* not exist in the map, default to undefined.
* @return {*} The value corresponding to the given key, or opt_default
* if the key does not exist in this map.
*/
goog.labs.structs.Map.prototype.get = function(key, opt_default) {
this.assertKeyIsString_(key);
if (this.hasKeyInPrimaryStore_(key)) {
return this.map_[key];
}
var index = goog.array.indexOf(this.secondaryStoreKeys_, key);
return index >= 0 ? this.secondaryStoreValues_[index] : opt_default;
};
/**
* Removes the map entry with the given key.
* @param {string} key The key to remove.
* @return {boolean} True if the entry is removed.
*/
goog.labs.structs.Map.prototype.remove = function(key) {
this.assertKeyIsString_(key);
if (this.hasKeyInPrimaryStore_(key)) {
this.count_--;
delete this.map_[key];
return true;
} else {
var index = goog.array.indexOf(this.secondaryStoreKeys_, key);
if (index >= 0) {
this.count_--;
goog.array.removeAt(this.secondaryStoreKeys_, index);
goog.array.removeAt(this.secondaryStoreValues_, index);
return true;
}
}
return false;
};
/**
* Adds the content of the map to this map. If a new entry uses a key
* that already exists in this map, the existing key is replaced.
* @param {!goog.labs.structs.Map} map The map to add.
*/
goog.labs.structs.Map.prototype.addAll = function(map) {
goog.array.forEach(map.getKeys(), function(key) {
this.set(key, map.get(key));
}, this);
};
/**
* @return {boolean} True if the map is empty.
*/
goog.labs.structs.Map.prototype.isEmpty = function() {
return !this.count_;
};
/**
* @return {number} The number of the entries in this map.
*/
goog.labs.structs.Map.prototype.getCount = function() {
return this.count_;
};
/**
* @param {string} key The key to check.
* @return {boolean} True if the map contains the given key.
*/
goog.labs.structs.Map.prototype.containsKey = function(key) {
this.assertKeyIsString_(key);
return this.hasKeyInPrimaryStore_(key) ||
goog.array.contains(this.secondaryStoreKeys_, key);
};
/**
* Whether the map contains the given value. The comparison is done
* using !== comparator. Also returns true if the passed value is NaN
* and a NaN value exists in the map.
* @param {*} value Value to check.
* @return {boolean} True if the map contains the given value.
*/
goog.labs.structs.Map.prototype.containsValue = function(value) {
var found = goog.object.some(this.map_, function(v, k) {
return this.hasKeyInPrimaryStore_(k) &&
goog.labs.object.is(v, value);
}, this);
return found || goog.array.contains(this.secondaryStoreValues_, value);
};
/**
* @return {!Array<string>} An array of all the keys contained in this map.
*/
goog.labs.structs.Map.prototype.getKeys = function() {
var keys;
if (goog.labs.structs.Map.BrowserFeature.OBJECT_KEYS_SUPPORTED) {
keys = goog.array.clone(Object.keys(this.map_));
} else {
keys = [];
for (var key in this.map_) {
if (goog.labs.structs.Map.objectHasOwnProperty_.call(this.map_, key)) {
keys.push(key);
}
}
}
goog.array.extend(keys, this.secondaryStoreKeys_);
return keys;
};
/**
* @return {!Array<*>} An array of all the values contained in this map.
* There may be duplicates.
*/
goog.labs.structs.Map.prototype.getValues = function() {
var values = [];
var keys = this.getKeys();
for (var i = 0; i < keys.length; i++) {
values.push(this.get(keys[i]));
}
return values;
};
/**
* @return {!Array<Array<?>>} An array of entries. Each entry is of the
* form [key, value]. Do not rely on consistent ordering of entries.
*/
goog.labs.structs.Map.prototype.getEntries = function() {
var entries = [];
var keys = this.getKeys();
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
entries.push([key, this.get(key)]);
}
return entries;
};
/**
* Clears the map to the initial state.
*/
goog.labs.structs.Map.prototype.clear = function() {
this.map_ = goog.labs.structs.Map.BrowserFeature.OBJECT_CREATE_SUPPORTED ?
Object.create(null) : {};
this.secondaryStoreKeys_ = [];
this.secondaryStoreValues_ = [];
this.count_ = 0;
};
/**
* Clones this map.
* @return {!goog.labs.structs.Map} The clone of this map.
*/
goog.labs.structs.Map.prototype.clone = function() {
var map = new goog.labs.structs.Map();
map.addAll(this);
return map;
};
/**
* @param {string} key The key to check.
* @return {boolean} True if the given key has been added successfully
* to the primary store.
* @private
*/
goog.labs.structs.Map.prototype.hasKeyInPrimaryStore_ = function(key) {
// New browsers that support Object.create do not allow setting of
// __proto__. In other browsers, hasOwnProperty will return true for
// __proto__ for object created with literal {}, so we need to
// special case it.
if (key == '__proto__') {
return false;
}
if (goog.labs.structs.Map.BrowserFeature.OBJECT_CREATE_SUPPORTED) {
return key in this.map_;
}
return goog.labs.structs.Map.objectHasOwnProperty_.call(this.map_, key);
};
/**
* Asserts that the given key is a string.
* @param {string} key The key to check.
* @private
*/
goog.labs.structs.Map.prototype.assertKeyIsString_ = function(key) {
goog.asserts.assert(goog.isString(key), 'key must be a string.');
};
/**
* Browser feature enum necessary for map.
* @enum {boolean}
*/
goog.labs.structs.Map.BrowserFeature = {
// TODO(chrishenry): Replace with goog.userAgent detection.
/**
* Whether Object.create method is supported.
*/
OBJECT_CREATE_SUPPORTED: !!Object.create,
/**
* Whether Object.keys method is supported.
*/
OBJECT_KEYS_SUPPORTED: !!Object.keys
};
@@ -0,0 +1,204 @@
// Copyright 2012 The Closure Library 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 Performance test for goog.structs.Map and
* goog.labs.structs.Map. To run this test fairly, you would have to
* compile this via JsCompiler (with --export_test_functions), and
* pull the compiled JS into an empty HTML file.
* @author chrishenry@google.com (Chris Henry)
*/
goog.provide('goog.labs.structs.MapPerf');
goog.setTestOnly('goog.labs.structs.MapPerf');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.labs.structs.Map');
goog.require('goog.structs.Map');
goog.require('goog.testing.PerformanceTable');
goog.require('goog.testing.jsunit');
goog.scope(function() {
var MapPerf = goog.labs.structs.MapPerf;
/**
* @typedef {goog.labs.structs.Map|goog.structs.Map}
*/
MapPerf.MapType;
/**
* @type {goog.testing.PerformanceTable}
*/
MapPerf.perfTable;
/**
* A key list. This maps loop index to key name to be used during
* benchmark. This ensure that we do not need to pay the cost of
* string concatenation/GC whenever we derive a key from loop index.
*
* This is filled once in setUpPage and then remain unchanged for the
* rest of the test case.
*
* @type {!Array<string>}
*/
MapPerf.keyList = [];
/**
* Maxium number of keys in keyList (and, by extension, the map under
* test).
* @type {number}
*/
MapPerf.MAX_NUM_KEY = 10000;
/**
* Fills the given map with generated key-value pair.
* @param {MapPerf.MapType} map The map to fill.
* @param {number} numKeys The number of key-value pair to fill.
*/
MapPerf.fillMap = function(map, numKeys) {
goog.asserts.assert(numKeys <= MapPerf.MAX_NUM_KEY);
for (var i = 0; i < numKeys; ++i) {
map.set(MapPerf.keyList[i], i);
}
};
/**
* Primes the given map with deletion of keys.
* @param {MapPerf.MapType} map The map to prime.
* @return {MapPerf.MapType} The primed map (for chaining).
*/
MapPerf.primeMapWithDeletion = function(map) {
for (var i = 0; i < 1000; ++i) {
map.set(MapPerf.keyList[i], i);
}
for (var i = 0; i < 1000; ++i) {
map.remove(MapPerf.keyList[i]);
}
return map;
};
/**
* Runs performance test for Map#get with the given map.
* @param {MapPerf.MapType} map The map to stress.
* @param {string} message Message to be put in performance table.
*/
MapPerf.runPerformanceTestForMapGet = function(map, message) {
MapPerf.fillMap(map, 10000);
MapPerf.perfTable.run(
function() {
// Creates local alias for map and keyList.
var localMap = map;
var localKeyList = MapPerf.keyList;
for (var i = 0; i < 500; ++i) {
var sum = 0;
for (var j = 0; j < 10000; ++j) {
sum += localMap.get(localKeyList[j]);
}
}
},
message);
};
/**
* Runs performance test for Map#set with the given map.
* @param {MapPerf.MapType} map The map to stress.
* @param {string} message Message to be put in performance table.
*/
MapPerf.runPerformanceTestForMapSet = function(map, message) {
MapPerf.perfTable.run(
function() {
// Creates local alias for map and keyList.
var localMap = map;
var localKeyList = MapPerf.keyList;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 10000; ++j) {
localMap.set(localKeyList[i], i);
}
}
},
message);
};
goog.global['setUpPage'] = function() {
var content = goog.dom.createDom('div');
goog.dom.insertChildAt(document.body, content, 0);
var ua = navigator.userAgent;
content.innerHTML =
'<h1>Closure Performance Tests - Map</h1>' +
'<p><strong>User-agent: </strong><span id="ua">' + ua + '</span></p>' +
'<div id="perf-table"></div>' +
'<hr>';
MapPerf.perfTable = new goog.testing.PerformanceTable(
goog.dom.getElement('perf-table'));
// Fills keyList.
for (var i = 0; i < MapPerf.MAX_NUM_KEY; ++i) {
MapPerf.keyList.push('k' + i);
}
};
goog.global['testGetFromLabsMap'] = function() {
MapPerf.runPerformanceTestForMapGet(
new goog.labs.structs.Map(), '#get: no previous deletion (Labs)');
};
goog.global['testGetFromOriginalMap'] = function() {
MapPerf.runPerformanceTestForMapGet(
new goog.structs.Map(), '#get: no previous deletion (Original)');
};
goog.global['testGetWithPreviousDeletionFromLabsMap'] = function() {
MapPerf.runPerformanceTestForMapGet(
MapPerf.primeMapWithDeletion(new goog.labs.structs.Map()),
'#get: with previous deletion (Labs)');
};
goog.global['testGetWithPreviousDeletionFromOriginalMap'] = function() {
MapPerf.runPerformanceTestForMapGet(
MapPerf.primeMapWithDeletion(new goog.structs.Map()),
'#get: with previous deletion (Original)');
};
goog.global['testSetFromLabsMap'] = function() {
MapPerf.runPerformanceTestForMapSet(
new goog.labs.structs.Map(), '#set: no previous deletion (Labs)');
};
goog.global['testSetFromOriginalMap'] = function() {
MapPerf.runPerformanceTestForMapSet(
new goog.structs.Map(), '#set: no previous deletion (Original)');
};
}); // goog.scope
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: chrishenry@google.com (Chris Henry)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.structs.Map
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.structs.MapTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,432 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.structs.MapTest');
goog.setTestOnly('goog.labs.structs.MapTest');
goog.require('goog.labs.structs.Map');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
var map;
var stubs;
function setUpPage() {
stubs = new goog.testing.PropertyReplacer();
}
function setUp() {
map = new goog.labs.structs.Map();
}
function testSet() {
var key = 'test';
var value = 'value';
map.set(key, value);
assertEquals(value, map.get(key));
}
function testSetsWithSameKey() {
var key = 'test';
var value = 'value';
var value2 = 'value2';
map.set(key, value);
map.set(key, value2);
assertEquals(value2, map.get(key));
}
function testSetWithUndefinedValue() {
var key = 'test';
map.set(key, undefined);
assertUndefined(map.get(key));
}
function testSetWithUnderUnderProtoUnderUnder() {
var key = '__proto__';
var value = 'value';
var value2 = 'value2';
map.set(key, value);
assertEquals(value, map.get(key));
map.set(key, value2);
assertEquals(value2, map.get(key));
}
function testSetWithBuiltInPropertyShadows() {
var key = 'toString';
var value = 'value';
var key2 = 'hasOwnProperty';
var value2 = 'value2';
map.set(key, value);
map.set(key2, value2);
assertEquals(value, map.get(key));
assertEquals(value2, map.get(key2));
map.set(key, value2);
map.set(key2, value);
assertEquals(value2, map.get(key));
assertEquals(value, map.get(key2));
}
function testGetBeforeSetOfUnderUnderProtoUnderUnder() {
assertUndefined(map.get('__proto__'));
}
function testContainsKey() {
assertFalse(map.containsKey('key'));
assertFalse(map.containsKey('__proto__'));
assertFalse(map.containsKey('toString'));
assertFalse(map.containsKey('hasOwnProperty'));
assertFalse(map.containsKey('key2'));
assertFalse(map.containsKey('key3'));
assertFalse(map.containsKey('key4'));
map.set('key', 'v');
map.set('__proto__', 'v');
map.set('toString', 'v');
map.set('hasOwnProperty', 'v');
map.set('key2', undefined);
map.set('key3', null);
map.set('key4', '');
assertTrue(map.containsKey('key'));
assertTrue(map.containsKey('__proto__'));
assertTrue(map.containsKey('toString'));
assertTrue(map.containsKey('hasOwnProperty'));
assertTrue(map.containsKey('key2'));
assertTrue(map.containsKey('key3'));
assertTrue(map.containsKey('key4'));
}
function testContainsValueWithShadowKeys() {
assertFalse(map.containsValue('v2'));
assertFalse(map.containsValue('v3'));
assertFalse(map.containsValue('v4'));
map.set('__proto__', 'v2');
map.set('toString', 'v3');
map.set('hasOwnProperty', 'v4');
assertTrue(map.containsValue('v2'));
assertTrue(map.containsValue('v3'));
assertTrue(map.containsValue('v4'));
assertFalse(map.containsValue(Object.prototype.toString));
assertFalse(map.containsValue(Object.prototype.hasOwnProperty));
}
function testContainsValueWithNullAndUndefined() {
assertFalse(map.containsValue(undefined));
assertFalse(map.containsValue(null));
map.set('key2', undefined);
map.set('key3', null);
assertTrue(map.containsValue(undefined));
assertTrue(map.containsValue(null));
}
function testContainsValueWithNumber() {
assertFalse(map.containsValue(-1));
assertFalse(map.containsValue(0));
assertFalse(map.containsValue(1));
map.set('key', -1);
map.set('key2', 0);
map.set('key3', 1);
assertTrue(map.containsValue(-1));
assertTrue(map.containsValue(0));
assertTrue(map.containsValue(1));
}
function testContainsValueWithNaN() {
assertFalse(map.containsValue(NaN));
map.set('key', NaN);
assertTrue(map.containsValue(NaN));
}
function testContainsValueWithNegativeZero() {
assertFalse(map.containsValue(-0));
map.set('key', -0);
assertTrue(map.containsValue(-0));
assertFalse(map.containsValue(0));
map.set('key', 0);
assertFalse(map.containsValue(-0));
assertTrue(map.containsValue(0));
}
function testContainsValueWithStrings() {
assertFalse(map.containsValue(''));
assertFalse(map.containsValue('v'));
map.set('key', '');
map.set('key2', 'v');
assertTrue(map.containsValue(''));
assertTrue(map.containsValue('v'));
}
function testRemove() {
map.set('key', 'v');
map.set('__proto__', 'v2');
map.set('toString', 'v3');
map.set('hasOwnProperty', 'v4');
map.set('key2', undefined);
map.set('key3', null);
map.set('key4', '');
assertFalse(map.remove('key do not exist'));
assertTrue(map.remove('key'));
assertFalse(map.containsKey('key'));
assertFalse(map.remove('key'));
assertTrue(map.remove('__proto__'));
assertFalse(map.containsKey('__proto__'));
assertFalse(map.remove('__proto__'));
assertTrue(map.remove('toString'));
assertFalse(map.containsKey('toString'));
assertFalse(map.remove('toString'));
assertTrue(map.remove('hasOwnProperty'));
assertFalse(map.containsKey('hasOwnProperty'));
assertFalse(map.remove('hasOwnProperty'));
assertTrue(map.remove('key2'));
assertFalse(map.containsKey('key2'));
assertFalse(map.remove('key2'));
assertTrue(map.remove('key3'));
assertFalse(map.containsKey('key3'));
assertFalse(map.remove('key3'));
assertTrue('', map.remove('key4'));
assertFalse(map.containsKey('key4'));
assertFalse(map.remove('key4'));
}
function testGetCountAndIsEmpty() {
assertEquals(0, map.getCount());
assertTrue(map.isEmpty());
map.set('key', 'v');
assertEquals(1, map.getCount());
map.set('__proto__', 'v2');
assertEquals(2, map.getCount());
map.set('toString', 'v3');
assertEquals(3, map.getCount());
map.set('hasOwnProperty', 'v4');
assertEquals(4, map.getCount());
map.set('key', 'a');
assertEquals(4, map.getCount());
map.set('__proto__', 'a2');
assertEquals(4, map.getCount());
map.set('toString', 'a3');
assertEquals(4, map.getCount());
map.set('hasOwnProperty', 'a4');
assertEquals(4, map.getCount());
map.remove('key');
assertEquals(3, map.getCount());
map.remove('__proto__');
assertEquals(2, map.getCount());
map.remove('toString');
assertEquals(1, map.getCount());
map.remove('hasOwnProperty');
assertEquals(0, map.getCount());
}
function testClear() {
map.set('key', 'v');
map.set('__proto__', 'v');
map.set('toString', 'v');
map.set('hasOwnProperty', 'v');
map.set('key2', undefined);
map.set('key3', null);
map.set('key4', '');
map.clear();
assertFalse(map.containsKey('key'));
assertFalse(map.containsKey('__proto__'));
assertFalse(map.containsKey('toString'));
assertFalse(map.containsKey('hasOwnProperty'));
assertFalse(map.containsKey('key2'));
assertFalse(map.containsKey('key3'));
assertFalse(map.containsKey('key4'));
}
function testGetEntries() {
map.set('key', 'v');
map.set('__proto__', 'v');
map.set('toString', 'v');
map.set('hasOwnProperty', 'v');
map.set('key2', undefined);
map.set('key3', null);
map.set('key4', '');
var entries = map.getEntries();
assertEquals(7, entries.length);
assertContainsEntry(['key', 'v'], entries);
assertContainsEntry(['__proto__', 'v'], entries);
assertContainsEntry(['toString', 'v'], entries);
assertContainsEntry(['hasOwnProperty', 'v'], entries);
assertContainsEntry(['key2', undefined], entries);
assertContainsEntry(['key3', null], entries);
assertContainsEntry(['key4', ''], entries);
}
function testGetKeys() {
map.set('key', 'v');
map.set('__proto__', 'v');
map.set('toString', 'v');
map.set('hasOwnProperty', 'v');
map.set('key2', undefined);
map.set('key3', null);
map.set('k4', '');
var values = map.getKeys();
assertSameElements(
['key', '__proto__', 'toString', 'hasOwnProperty', 'key2', 'key3', 'k4'],
values);
}
function testGetValues() {
map.set('key', 'v');
map.set('__proto__', 'v');
map.set('toString', 'v');
map.set('hasOwnProperty', 'v');
map.set('key2', undefined);
map.set('key3', null);
map.set('key4', '');
var values = map.getValues();
assertSameElements(['v', 'v', 'v', 'v', undefined, null, ''], values);
}
function testAddAllToEmptyMap() {
map.set('key', 'v');
map.set('key2', 'v2');
map.set('key3', 'v3');
map.set('key4', 'v4');
var map2 = new goog.labs.structs.Map();
map2.addAll(map);
assertEquals(4, map2.getCount());
assertEquals('v', map2.get('key'));
assertEquals('v2', map2.get('key2'));
assertEquals('v3', map2.get('key3'));
assertEquals('v4', map2.get('key4'));
}
function testAddAllToNonEmptyMap() {
map.set('key', 'v');
map.set('key2', 'v2');
map.set('key3', 'v3');
map.set('key4', 'v4');
var map2 = new goog.labs.structs.Map();
map2.set('key0', 'o');
map2.set('key', 'o');
map2.set('key2', 'o2');
map2.set('key3', 'o3');
map2.addAll(map);
assertEquals(5, map2.getCount());
assertEquals('o', map2.get('key0'));
assertEquals('v', map2.get('key'));
assertEquals('v2', map2.get('key2'));
assertEquals('v3', map2.get('key3'));
assertEquals('v4', map2.get('key4'));
}
function testClone() {
map.set('key', 'v');
map.set('key2', 'v2');
map.set('key3', 'v3');
map.set('key4', 'v4');
var map2 = map.clone();
assertEquals(4, map2.getCount());
assertEquals('v', map2.get('key'));
assertEquals('v2', map2.get('key2'));
assertEquals('v3', map2.get('key3'));
assertEquals('v4', map2.get('key4'));
}
function testMapWithModifiedObjectPrototype() {
stubs.set(Object.prototype, 'toString', function() {});
stubs.set(Object.prototype, 'foo', function() {});
stubs.set(Object.prototype, 'field', 100);
stubs.set(Object.prototype, 'fooKey', function() {});
map = new goog.labs.structs.Map();
map.set('key', 'v');
map.set('key2', 'v2');
map.set('fooKey', 'v3');
assertTrue(map.containsKey('key'));
assertTrue(map.containsKey('key2'));
assertTrue(map.containsKey('fooKey'));
assertFalse(map.containsKey('toString'));
assertFalse(map.containsKey('foo'));
assertFalse(map.containsKey('field'));
assertTrue(map.containsValue('v'));
assertTrue(map.containsValue('v2'));
assertTrue(map.containsValue('v3'));
assertFalse(map.containsValue(100));
var entries = map.getEntries();
assertEquals(3, entries.length);
assertContainsEntry(['key', 'v'], entries);
assertContainsEntry(['key2', 'v2'], entries);
assertContainsEntry(['fooKey', 'v3'], entries);
}
function assertContainsEntry(entry, entryList) {
for (var i = 0; i < entryList.length; ++i) {
if (entry[0] == entryList[i][0] && entry[1] === entryList[i][1]) {
return;
}
}
fail('Did not find entry: ' + entry + ' in: ' + entryList);
}
@@ -0,0 +1,282 @@
// Copyright 2012 The Closure Library 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 A collection similar to
* {@code goog.labs.structs.Map}, but also allows associating multiple
* values with a single key.
*
* This implementation ensures that you can use any string keys.
*
* @author chrishenry@google.com (Chris Henry)
*/
goog.provide('goog.labs.structs.Multimap');
goog.require('goog.array');
goog.require('goog.labs.object');
goog.require('goog.labs.structs.Map');
/**
* Creates a new multimap.
* @constructor
* @struct
* @final
*/
goog.labs.structs.Multimap = function() {
this.clear();
};
/**
* The backing map.
* @type {!goog.labs.structs.Map}
* @private
*/
goog.labs.structs.Multimap.prototype.map_;
/**
* @type {number}
* @private
*/
goog.labs.structs.Multimap.prototype.count_ = 0;
/**
* Clears the multimap.
*/
goog.labs.structs.Multimap.prototype.clear = function() {
this.count_ = 0;
this.map_ = new goog.labs.structs.Map();
};
/**
* Clones this multimap.
* @return {!goog.labs.structs.Multimap} A multimap that contains all
* the mapping this multimap has.
*/
goog.labs.structs.Multimap.prototype.clone = function() {
var map = new goog.labs.structs.Multimap();
map.addAllFromMultimap(this);
return map;
};
/**
* Adds the given (key, value) pair to the map. The (key, value) pair
* is guaranteed to be added.
* @param {string} key The key to add.
* @param {*} value The value to add.
*/
goog.labs.structs.Multimap.prototype.add = function(key, value) {
var values = this.map_.get(key);
if (!values) {
this.map_.set(key, (values = []));
}
values.push(value);
this.count_++;
};
/**
* Stores a collection of values to the given key. Does not replace
* existing (key, value) pairs.
* @param {string} key The key to add.
* @param {!Array<*>} values The values to add.
*/
goog.labs.structs.Multimap.prototype.addAllValues = function(key, values) {
goog.array.forEach(values, function(v) {
this.add(key, v);
}, this);
};
/**
* Adds the contents of the given map/multimap to this multimap.
* @param {!(goog.labs.structs.Map|goog.labs.structs.Multimap)} map The
* map to add.
*/
goog.labs.structs.Multimap.prototype.addAllFromMultimap = function(map) {
goog.array.forEach(map.getEntries(), function(entry) {
this.add(entry[0], entry[1]);
}, this);
};
/**
* Replaces all the values for the given key with the given values.
* @param {string} key The key whose values are to be replaced.
* @param {!Array<*>} values The new values. If empty, this is
* equivalent to {@code removaAll(key)}.
*/
goog.labs.structs.Multimap.prototype.replaceValues = function(key, values) {
this.removeAll(key);
this.addAllValues(key, values);
};
/**
* Gets the values correspond to the given key.
* @param {string} key The key to retrieve.
* @return {!Array<*>} An array of values corresponding to the given
* key. May be empty. Note that the ordering of values are not
* guaranteed to be consistent.
*/
goog.labs.structs.Multimap.prototype.get = function(key) {
var values = /** @type {Array<*>} */ (this.map_.get(key));
return values ? goog.array.clone(values) : [];
};
/**
* Removes a single occurrence of (key, value) pair.
* @param {string} key The key to remove.
* @param {*} value The value to remove.
* @return {boolean} Whether any matching (key, value) pair is removed.
*/
goog.labs.structs.Multimap.prototype.remove = function(key, value) {
var values = /** @type {Array<*>} */ (this.map_.get(key));
if (!values) {
return false;
}
var removed = goog.array.removeIf(values, function(v) {
return goog.labs.object.is(value, v);
});
if (removed) {
this.count_--;
if (values.length == 0) {
this.map_.remove(key);
}
}
return removed;
};
/**
* Removes all values corresponding to the given key.
* @param {string} key The key whose values are to be removed.
* @return {boolean} Whether any value is removed.
*/
goog.labs.structs.Multimap.prototype.removeAll = function(key) {
// We have to first retrieve the values from the backing map because
// we need to keep track of count (and correctly calculates the
// return value). values may be undefined.
var values = this.map_.get(key);
if (this.map_.remove(key)) {
this.count_ -= values.length;
return true;
}
return false;
};
/**
* @return {boolean} Whether the multimap is empty.
*/
goog.labs.structs.Multimap.prototype.isEmpty = function() {
return !this.count_;
};
/**
* @return {number} The count of (key, value) pairs in the map.
*/
goog.labs.structs.Multimap.prototype.getCount = function() {
return this.count_;
};
/**
* @param {string} key The key to check.
* @param {*} value The value to check.
* @return {boolean} Whether the (key, value) pair exists in the multimap.
*/
goog.labs.structs.Multimap.prototype.containsEntry = function(key, value) {
var values = /** @type {Array<*>} */ (this.map_.get(key));
if (!values) {
return false;
}
var index = goog.array.findIndex(values, function(v) {
return goog.labs.object.is(v, value);
});
return index >= 0;
};
/**
* @param {string} key The key to check.
* @return {boolean} Whether the multimap contains at least one (key,
* value) pair with the given key.
*/
goog.labs.structs.Multimap.prototype.containsKey = function(key) {
return this.map_.containsKey(key);
};
/**
* @param {*} value The value to check.
* @return {boolean} Whether the multimap contains at least one (key,
* value) pair with the given value.
*/
goog.labs.structs.Multimap.prototype.containsValue = function(value) {
return goog.array.some(this.map_.getValues(),
function(values) {
return goog.array.some(/** @type {Array<?>} */ (values), function(v) {
return goog.labs.object.is(v, value);
});
});
};
/**
* @return {!Array<string>} An array of unique keys.
*/
goog.labs.structs.Multimap.prototype.getKeys = function() {
return this.map_.getKeys();
};
/**
* @return {!Array<*>} An array of values. There may be duplicates.
*/
goog.labs.structs.Multimap.prototype.getValues = function() {
return goog.array.flatten(this.map_.getValues());
};
/**
* @return {!Array<!Array<?>>} An array of entries. Each entry is of the
* form [key, value].
*/
goog.labs.structs.Multimap.prototype.getEntries = function() {
var keys = this.getKeys();
var entries = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var values = this.get(key);
for (var j = 0; j < values.length; j++) {
entries.push([key, values[j]]);
}
}
return entries;
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: chrishenry@google.com (Chris Henry)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.labs.structs.Multimap
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.structs.MultimapTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,328 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.structs.MultimapTest');
goog.setTestOnly('goog.labs.structs.MultimapTest');
goog.require('goog.labs.structs.Map');
goog.require('goog.labs.structs.Multimap');
goog.require('goog.testing.jsunit');
var map;
function setUp() {
map = new goog.labs.structs.Multimap();
}
function testGetCountWithEmptyMultimap() {
assertEquals(0, map.getCount());
assertTrue(map.isEmpty());
}
function testClone() {
map.add('k', 'v');
map.addAllValues('k2', ['v', 'v1', 'v2']);
var map2 = map.clone();
assertSameElements(['v'], map.get('k'));
assertSameElements(['v', 'v1', 'v2'], map.get('k2'));
}
function testAdd() {
map.add('key', 'v');
assertEquals(1, map.getCount());
map.add('key', 'v2');
assertEquals(2, map.getCount());
map.add('key', 'v3');
assertEquals(3, map.getCount());
var values = map.get('key');
assertEquals(3, values.length);
assertContains('v', values);
assertContains('v2', values);
assertContains('v3', values);
}
function testAddValues() {
map.addAllValues('key', ['v', 'v2', 'v3']);
assertSameElements(['v', 'v2', 'v3'], map.get('key'));
map.add('key2', 'a');
map.addAllValues('key2', ['v', 'v2', 'v3']);
assertSameElements(['a', 'v', 'v2', 'v3'], map.get('key2'));
}
function testAddAllWithMultimap() {
map.add('k', 'v');
map.addAllValues('k2', ['v', 'v1', 'v2']);
var map2 = new goog.labs.structs.Multimap();
map2.add('k2', 'v');
map2.addAllValues('k3', ['a', 'a1', 'a2']);
map.addAllFromMultimap(map2);
assertSameElements(['v'], map.get('k'));
assertSameElements(['v', 'v1', 'v2', 'v'], map.get('k2'));
assertSameElements(['a', 'a1', 'a2'], map.get('k3'));
}
function testAddAllWithMap() {
map.add('k', 'v');
map.addAllValues('k2', ['v', 'v1', 'v2']);
var map2 = new goog.labs.structs.Map();
map2.set('k2', 'v');
map2.set('k3', 'a');
map.addAllFromMultimap(map2);
assertSameElements(['v'], map.get('k'));
assertSameElements(['v', 'v1', 'v2', 'v'], map.get('k2'));
assertSameElements(['a'], map.get('k3'));
}
function testReplaceValues() {
map.add('key', 'v');
map.add('key', 'v2');
map.replaceValues('key', [0, 1, 2]);
assertSameElements([0, 1, 2], map.get('key'));
assertEquals(3, map.getCount());
map.replaceValues('key', ['v']);
assertSameElements(['v'], map.get('key'));
assertEquals(1, map.getCount());
map.replaceValues('key', []);
assertSameElements([], map.get('key'));
assertEquals(0, map.getCount());
}
function testRemove() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key', 'v3');
assertTrue(map.remove('key', 'v'));
var values = map.get('key');
assertEquals(2, map.getCount());
assertEquals(2, values.length);
assertContains('v2', values);
assertContains('v3', values);
assertFalse(map.remove('key', 'v'));
assertTrue(map.remove('key', 'v2'));
values = map.get('key');
assertEquals(1, map.getCount());
assertEquals(1, values.length);
assertContains('v3', values);
assertFalse(map.remove('key', 'v2'));
assertTrue(map.remove('key', 'v3'));
map.remove('key', 'v3');
assertTrue(map.isEmpty());
assertEquals(0, map.get('key').length);
assertFalse(map.remove('key', 'v2'));
}
function testRemoveWithNaN() {
map.add('key', NaN);
map.add('key', NaN);
assertTrue(map.remove('key', NaN));
var values = map.get('key');
assertEquals(1, values.length);
assertTrue(isNaN(values[0]));
assertTrue(map.remove('key', NaN));
assertEquals(0, map.get('key').length);
assertFalse(map.remove('key', NaN));
}
function testRemoveWithNegativeZero() {
map.add('key', 0);
map.add('key', -0);
assertTrue(map.remove('key', -0));
var values = map.get('key');
assertEquals(1, values.length);
assertTrue(1 / values[0] === 1 / 0);
assertFalse(map.remove('key', -0));
map.add('key', -0);
assertTrue(map.remove('key', 0));
var values = map.get('key');
assertEquals(1, values.length);
assertTrue(1 / values[0] === 1 / -0);
assertFalse(map.remove('key', 0));
assertTrue(map.remove('key', -0));
assertEquals(0, map.get('key').length);
}
function testRemoveAll() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key', 'v3');
map.add('key', 'v4');
map.add('key2', 'v');
assertTrue(map.removeAll('key'));
assertSameElements([], map.get('key'));
assertSameElements(['v'], map.get('key2'));
assertFalse(map.removeAll('key'));
assertEquals(1, map.getCount());
assertTrue(map.removeAll('key2'));
assertSameElements([], map.get('key2'));
assertFalse(map.removeAll('key2'));
assertTrue(map.isEmpty());
}
function testAddWithDuplicateValue() {
map.add('key', 'v');
map.add('key', 'v');
map.add('key', 'v');
assertArrayEquals(['v', 'v', 'v'], map.get('key'));
}
function testContainsEntry() {
assertFalse(map.containsEntry('k', 'v'));
assertFalse(map.containsEntry('k', 'v2'));
assertFalse(map.containsEntry('k2', 'v'));
map.add('k', 'v');
assertTrue(map.containsEntry('k', 'v'));
assertFalse(map.containsEntry('k', 'v2'));
assertFalse(map.containsEntry('k2', 'v'));
map.add('k', 'v2');
assertTrue(map.containsEntry('k', 'v'));
assertTrue(map.containsEntry('k', 'v2'));
assertFalse(map.containsEntry('k2', 'v'));
map.add('k2', 'v');
assertTrue(map.containsEntry('k', 'v'));
assertTrue(map.containsEntry('k', 'v2'));
assertTrue(map.containsEntry('k2', 'v'));
}
function testContainsKey() {
assertFalse(map.containsKey('k'));
assertFalse(map.containsKey('k2'));
map.add('k', 'v');
assertTrue(map.containsKey('k'));
map.add('k2', 'v');
assertTrue(map.containsKey('k2'));
map.remove('k', 'v');
assertFalse(map.containsKey('k'));
map.remove('k2', 'v');
assertFalse(map.containsKey('k2'));
}
function testContainsValue() {
assertFalse(map.containsValue('v'));
assertFalse(map.containsValue('v2'));
map.add('key', 'v');
assertTrue(map.containsValue('v'));
map.add('key', 'v2');
assertTrue(map.containsValue('v2'));
}
function testGetEntries() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key2', 'v3');
var entries = map.getEntries();
assertEquals(3, entries.length);
assertContainsEntry(['key', 'v'], entries);
assertContainsEntry(['key', 'v2'], entries);
assertContainsEntry(['key2', 'v3'], entries);
}
function testGetKeys() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key2', 'v3');
map.add('key3', 'v4');
map.removeAll('key3');
assertSameElements(['key', 'key2'], map.getKeys());
}
function testGetKeys() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key2', 'v2');
map.add('key3', 'v4');
map.removeAll('key3');
assertSameElements(['v', 'v2', 'v2'], map.getValues());
}
function testGetReturnsDefensiveCopyOfUnderlyingData() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key', 'v3');
var values = map.get('key');
values.push('v4');
assertFalse(map.containsEntry('key', 'v4'));
}
function testClear() {
map.add('key', 'v');
map.add('key', 'v2');
map.add('key2', 'v3');
map.clear();
assertTrue(map.isEmpty());
assertSameElements([], map.getEntries());
}
function assertContainsEntry(entry, entryList) {
for (var i = 0; i < entryList.length; ++i) {
if (entry[0] == entryList[i][0] && entry[1] === entryList[i][1]) {
return;
}
}
fail('Did not find entry: ' + entry + ' in: ' + entryList);
}
@@ -0,0 +1,179 @@
// Copyright 2013 The Closure Library 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 Utility class that monitors pixel density ratio changes.
*
* @see ../demos/pixeldensitymonitor.html
*/
goog.provide('goog.labs.style.PixelDensityMonitor');
goog.provide('goog.labs.style.PixelDensityMonitor.Density');
goog.provide('goog.labs.style.PixelDensityMonitor.EventType');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
/**
* Monitors the window for changes to the ratio between device and screen
* pixels, e.g. when the user moves the window from a high density screen to a
* screen with normal density. Dispatches
* goog.labs.style.PixelDensityMonitor.EventType.CHANGE events when the density
* changes between the two predefined values NORMAL and HIGH.
*
* This class uses the window.devicePixelRatio value which is supported in
* WebKit and FF18. If the value does not exist, it will always return a
* NORMAL density. It requires support for MediaQueryList to detect changes to
* the devicePixelRatio.
*
* @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper which contains the
* document associated with the window to listen to. Defaults to the one in
* which this code is executing.
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.labs.style.PixelDensityMonitor = function(opt_domHelper) {
goog.labs.style.PixelDensityMonitor.base(this, 'constructor');
/**
* @type {Window}
* @private
*/
this.window_ = opt_domHelper ? opt_domHelper.getWindow() : window;
/**
* The last density that was reported so that changes can be detected.
* @type {goog.labs.style.PixelDensityMonitor.Density}
* @private
*/
this.lastDensity_ = this.getDensity();
/**
* @type {function (MediaQueryList)}
* @private
*/
this.listener_ = goog.bind(this.handleMediaQueryChange_, this);
/**
* The media query list for a query that detects high density, if supported
* by the browser. Because matchMedia returns a new object for every call, it
* needs to be saved here so the listener can be removed when disposing.
* @type {?MediaQueryList}
* @private
*/
this.mediaQueryList_ = this.window_.matchMedia ? this.window_.matchMedia(
goog.labs.style.PixelDensityMonitor.HIGH_DENSITY_QUERY_) : null;
};
goog.inherits(goog.labs.style.PixelDensityMonitor, goog.events.EventTarget);
/**
* The two different pixel density modes on which the various ratios between
* physical and device pixels are mapped.
* @enum {number}
*/
goog.labs.style.PixelDensityMonitor.Density = {
/**
* Mode for older portable devices and desktop screens, defined as having a
* device pixel ratio of less than 1.5.
*/
NORMAL: 1,
/**
* Mode for newer portable devices with a high resolution screen, defined as
* having a device pixel ratio of more than 1.5.
*/
HIGH: 2
};
/**
* The events fired by the PixelDensityMonitor.
* @enum {string}
*/
goog.labs.style.PixelDensityMonitor.EventType = {
/**
* Dispatched when density changes between NORMAL and HIGH.
*/
CHANGE: goog.events.getUniqueId('change')
};
/**
* Minimum ratio between device and screen pixel needed for high density mode.
* @type {number}
* @private
*/
goog.labs.style.PixelDensityMonitor.HIGH_DENSITY_RATIO_ = 1.5;
/**
* Media query that matches for high density.
* @type {string}
* @private
*/
goog.labs.style.PixelDensityMonitor.HIGH_DENSITY_QUERY_ =
'(min-resolution: 1.5dppx), (-webkit-min-device-pixel-ratio: 1.5)';
/**
* Starts monitoring for changes in pixel density.
*/
goog.labs.style.PixelDensityMonitor.prototype.start = function() {
if (this.mediaQueryList_) {
this.mediaQueryList_.addListener(this.listener_);
}
};
/**
* @return {goog.labs.style.PixelDensityMonitor.Density} The density for the
* window.
*/
goog.labs.style.PixelDensityMonitor.prototype.getDensity = function() {
if (this.window_.devicePixelRatio >=
goog.labs.style.PixelDensityMonitor.HIGH_DENSITY_RATIO_) {
return goog.labs.style.PixelDensityMonitor.Density.HIGH;
} else {
return goog.labs.style.PixelDensityMonitor.Density.NORMAL;
}
};
/**
* Handles a change to the media query and checks whether the density has
* changed since the last call.
* @param {MediaQueryList} mql The list of changed media queries.
* @private
*/
goog.labs.style.PixelDensityMonitor.prototype.handleMediaQueryChange_ =
function(mql) {
var newDensity = this.getDensity();
if (this.lastDensity_ != newDensity) {
this.lastDensity_ = newDensity;
this.dispatchEvent(goog.labs.style.PixelDensityMonitor.EventType.CHANGE);
}
};
/** @override */
goog.labs.style.PixelDensityMonitor.prototype.disposeInternal = function() {
if (this.mediaQueryList_) {
this.mediaQueryList_.removeListener(this.listener_);
}
goog.labs.style.PixelDensityMonitor.base(this, 'disposeInternal');
};
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>Tests for goog.labs.style.PixelDensityMonitor</title>
<script src="../../base.js"></script>
<script>
goog.require('goog.labs.style.PixelDensityMonitorTest');
</script>
<body>
</body>
</html>
@@ -0,0 +1,146 @@
// Copyright 2013 The Closure Library 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 Tests for goog.labs.style.PixelDensityMonitor.
*
*/
goog.provide('goog.labs.style.PixelDensityMonitorTest');
goog.setTestOnly('goog.labs.style.PixelDensityMonitorTest');
goog.require('goog.array');
goog.require('goog.dom.DomHelper');
goog.require('goog.events');
goog.require('goog.labs.style.PixelDensityMonitor');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var fakeWindow;
var recordFunction;
var monitor;
var mockControl;
var mediaQueryLists;
function setUp() {
recordFunction = goog.testing.recordFunction();
mediaQueryLists = [];
mockControl = new goog.testing.MockControl();
}
function tearDown() {
mockControl.$verifyAll();
goog.dispose(monitor);
goog.dispose(recordFunction);
}
function setUpMonitor(initialRatio, hasMatchMedia) {
fakeWindow = {
devicePixelRatio: initialRatio
};
if (hasMatchMedia) {
// Every call to matchMedia should return a new media query list with its
// own set of listeners.
fakeWindow.matchMedia = function(query) {
var listeners = [];
var newList = {
addListener: function(listener) {
listeners.push(listener);
},
removeListener: function(listener) {
goog.array.remove(listeners, listener);
},
callListeners: function() {
for (var i = 0; i < listeners.length; i++) {
listeners[i]();
}
},
getListenerCount: function() {
return listeners.length;
}
};
mediaQueryLists.push(newList);
return newList;
};
}
var domHelper = mockControl.createStrictMock(goog.dom.DomHelper);
domHelper.getWindow().$returns(fakeWindow);
mockControl.$replayAll();
monitor = new goog.labs.style.PixelDensityMonitor(domHelper);
goog.events.listen(monitor,
goog.labs.style.PixelDensityMonitor.EventType.CHANGE, recordFunction);
}
function setNewRatio(newRatio) {
fakeWindow.devicePixelRatio = newRatio;
for (var i = 0; i < mediaQueryLists.length; i++) {
mediaQueryLists[i].callListeners();
}
}
function testNormalDensity() {
setUpMonitor(1, false);
assertEquals(goog.labs.style.PixelDensityMonitor.Density.NORMAL,
monitor.getDensity());
}
function testHighDensity() {
setUpMonitor(1.5, false);
assertEquals(goog.labs.style.PixelDensityMonitor.Density.HIGH,
monitor.getDensity());
}
function testNormalDensityIfUndefined() {
setUpMonitor(undefined, false);
assertEquals(goog.labs.style.PixelDensityMonitor.Density.NORMAL,
monitor.getDensity());
}
function testChangeEvent() {
setUpMonitor(1, true);
assertEquals(goog.labs.style.PixelDensityMonitor.Density.NORMAL,
monitor.getDensity());
monitor.start();
setNewRatio(2);
var call = recordFunction.popLastCall();
assertEquals(goog.labs.style.PixelDensityMonitor.Density.HIGH,
call.getArgument(0).target.getDensity());
assertEquals(goog.labs.style.PixelDensityMonitor.Density.HIGH,
monitor.getDensity());
setNewRatio(1);
call = recordFunction.popLastCall();
assertEquals(goog.labs.style.PixelDensityMonitor.Density.NORMAL,
call.getArgument(0).target.getDensity());
assertEquals(goog.labs.style.PixelDensityMonitor.Density.NORMAL,
monitor.getDensity());
}
function testListenerIsDisposed() {
setUpMonitor(1, true);
monitor.start();
assertEquals(1, mediaQueryLists.length);
assertEquals(1, mediaQueryLists[0].getListenerCount());
goog.dispose(monitor);
assertEquals(1, mediaQueryLists.length);
assertEquals(0, mediaQueryLists[0].getListenerCount());
}
@@ -0,0 +1,58 @@
// Copyright 2012 The Closure Library 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 Provides main functionality of assertThat. assertThat calls the
* matcher's matches method to test if a matcher matches assertThat's arguments.
*/
goog.provide('goog.labs.testing.MatcherError');
goog.provide('goog.labs.testing.assertThat');
goog.require('goog.debug.Error');
/**
* Asserts that the actual value evaluated by the matcher is true.
*
* @param {*} actual The object to assert by the matcher.
* @param {!goog.labs.testing.Matcher} matcher A matcher to verify values.
* @param {string=} opt_reason Description of what is asserted.
*
*/
goog.labs.testing.assertThat = function(actual, matcher, opt_reason) {
if (!matcher.matches(actual)) {
// Prefix the error description with a reason from the assert ?
var prefix = opt_reason ? opt_reason + ': ' : '';
var desc = prefix + matcher.describe(actual);
// some sort of failure here
throw new goog.labs.testing.MatcherError(desc);
}
};
/**
* Error thrown when a Matcher fails to match the input value.
* @param {string=} opt_message The error message.
* @constructor
* @extends {goog.debug.Error}
* @final
*/
goog.labs.testing.MatcherError = function(opt_message) {
goog.labs.testing.MatcherError.base(this, 'constructor', opt_message);
};
goog.inherits(goog.labs.testing.MatcherError, goog.debug.Error);
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - goog.labs.testing.assertThat
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.testing.assertThatTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,69 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.testing.assertThatTest');
goog.setTestOnly('goog.labs.testing.assertThatTest');
goog.require('goog.labs.testing.MatcherError');
goog.require('goog.labs.testing.assertThat');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var successMatchesFn, failureMatchesFn, describeFn, successTestMatcher;
var failureTestMatcher;
function setUp() {
successMatchesFn = new goog.testing.recordFunction(function() {return true;});
failureMatchesFn =
new goog.testing.recordFunction(function() {return false;});
describeFn = new goog.testing.recordFunction();
successTestMatcher = function() {
return { matches: successMatchesFn, describe: describeFn };
};
failureTestMatcher = function() {
return { matches: failureMatchesFn, describe: describeFn };
};
}
function testAssertthatAlwaysCallsMatches() {
var value = 7;
goog.labs.testing.assertThat(value, successTestMatcher(),
'matches is called on success');
assertEquals(1, successMatchesFn.getCallCount());
var matchesCall = successMatchesFn.popLastCall();
assertEquals(value, matchesCall.getArgument(0));
var e = assertThrows(goog.bind(goog.labs.testing.assertThat, null,
value, failureTestMatcher(), 'matches is called on failure'));
assertTrue(e instanceof goog.labs.testing.MatcherError);
assertEquals(1, failureMatchesFn.getCallCount());
}
function testAssertthatCallsDescribeOnFailure() {
var value = 7;
var e = assertThrows(goog.bind(goog.labs.testing.assertThat, null,
value, failureTestMatcher(), 'describe is called on failure'));
assertTrue(e instanceof goog.labs.testing.MatcherError);
assertEquals(1, failureMatchesFn.getCallCount());
assertEquals(1, describeFn.getCallCount());
var matchesCall = describeFn.popLastCall();
assertEquals(value, matchesCall.getArgument(0));
}
@@ -0,0 +1,95 @@
// Copyright 2012 The Closure Library 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 Provides the built-in decorators: is, describedAs, anything.
*/
goog.provide('goog.labs.testing.AnythingMatcher');
goog.require('goog.labs.testing.Matcher');
/**
* The Anything matcher. Matches all possible inputs.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.AnythingMatcher = function() {};
/**
* Matches anything. Useful if one doesn't care what the object under test is.
*
* @override
*/
goog.labs.testing.AnythingMatcher.prototype.matches =
function(actualObject) {
return true;
};
/**
* This method is never called but is needed so AnythingMatcher implements the
* Matcher interface.
*
* @override
*/
goog.labs.testing.AnythingMatcher.prototype.describe =
function(actualObject) {
throw Error('AnythingMatcher should never fail!');
};
/**
* Returns a matcher that matches anything.
*
* @return {!goog.labs.testing.AnythingMatcher} A AnythingMatcher.
*/
function anything() {
return new goog.labs.testing.AnythingMatcher();
}
/**
* Returnes any matcher that is passed to it (aids readability).
*
* @param {!goog.labs.testing.Matcher} matcher A matcher.
* @return {!goog.labs.testing.Matcher} The wrapped matcher.
*/
function is(matcher) {
return matcher;
}
/**
* Returns a matcher with a customized description for the given matcher.
*
* @param {string} description The custom description for the matcher.
* @param {!goog.labs.testing.Matcher} matcher The matcher.
*
* @return {!goog.labs.testing.Matcher} The matcher with custom description.
*/
function describedAs(description, matcher) {
matcher.describe = function(value) {
return description;
};
return matcher;
}
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - Decorator matchers
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.testing.decoratorMatcherTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,41 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.testing.decoratorMatcherTest');
goog.setTestOnly('goog.labs.testing.decoratorMatcherTest');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.AnythingMatcher');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.GreaterThanMatcher');
goog.require('goog.labs.testing.MatcherError');
goog.require('goog.labs.testing.assertThat');
goog.require('goog.testing.jsunit');
function testAnythingMatcher() {
goog.labs.testing.assertThat(true, anything(), 'anything matches true');
goog.labs.testing.assertThat(false, anything(), 'false matches anything');
}
function testIs() {
goog.labs.testing.assertThat(5, is(greaterThan(4)), '5 is > 4');
}
function testDescribedAs() {
var e = assertThrows(function() {
goog.labs.testing.assertThat(4, describedAs('this is a test',
greaterThan(6)))});
assertTrue(e instanceof goog.labs.testing.MatcherError);
assertEquals('this is a test', e.message);
}
@@ -0,0 +1,273 @@
// Copyright 2012 The Closure Library 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 Provides the built-in dictionary matcher methods like
* hasEntry, hasEntries, hasKey, hasValue, etc.
*/
goog.provide('goog.labs.testing.HasEntriesMatcher');
goog.provide('goog.labs.testing.HasEntryMatcher');
goog.provide('goog.labs.testing.HasKeyMatcher');
goog.provide('goog.labs.testing.HasValueMatcher');
goog.require('goog.asserts');
goog.require('goog.labs.testing.Matcher');
goog.require('goog.object');
/**
* The HasEntries matcher.
*
* @param {!Object} entries The entries to check in the object.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.HasEntriesMatcher = function(entries) {
/**
* @type {Object}
* @private
*/
this.entries_ = entries;
};
/**
* Determines if an object has particular entries.
*
* @override
*/
goog.labs.testing.HasEntriesMatcher.prototype.matches =
function(actualObject) {
goog.asserts.assertObject(actualObject, 'Expected an Object');
var object = /** @type {!Object} */(actualObject);
return goog.object.every(this.entries_, function(value, key) {
return goog.object.containsKey(object, key) &&
object[key] === value;
});
};
/**
* @override
*/
goog.labs.testing.HasEntriesMatcher.prototype.describe =
function(actualObject) {
goog.asserts.assertObject(actualObject, 'Expected an Object');
var object = /** @type {!Object} */(actualObject);
var errorString = 'Input object did not contain the following entries:\n';
goog.object.forEach(this.entries_, function(value, key) {
if (!goog.object.containsKey(object, key) ||
object[key] !== value) {
errorString += key + ': ' + value + '\n';
}
});
return errorString;
};
/**
* The HasEntry matcher.
*
* @param {string} key The key for the entry.
* @param {*} value The value for the key.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.HasEntryMatcher = function(key, value) {
/**
* @type {string}
* @private
*/
this.key_ = key;
/**
* @type {*}
* @private
*/
this.value_ = value;
};
/**
* Determines if an object has a particular entry.
*
* @override
*/
goog.labs.testing.HasEntryMatcher.prototype.matches =
function(actualObject) {
goog.asserts.assertObject(actualObject);
return goog.object.containsKey(actualObject, this.key_) &&
actualObject[this.key_] === this.value_;
};
/**
* @override
*/
goog.labs.testing.HasEntryMatcher.prototype.describe =
function(actualObject) {
goog.asserts.assertObject(actualObject);
var errorMsg;
if (goog.object.containsKey(actualObject, this.key_)) {
errorMsg = 'Input object did not contain key: ' + this.key_;
} else {
errorMsg = 'Value for key did not match value: ' + this.value_;
}
return errorMsg;
};
/**
* The HasKey matcher.
*
* @param {string} key The key to check in the object.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.HasKeyMatcher = function(key) {
/**
* @type {string}
* @private
*/
this.key_ = key;
};
/**
* Determines if an object has a key.
*
* @override
*/
goog.labs.testing.HasKeyMatcher.prototype.matches =
function(actualObject) {
goog.asserts.assertObject(actualObject);
return goog.object.containsKey(actualObject, this.key_);
};
/**
* @override
*/
goog.labs.testing.HasKeyMatcher.prototype.describe =
function(actualObject) {
goog.asserts.assertObject(actualObject);
return 'Input object did not contain the key: ' + this.key_;
};
/**
* The HasValue matcher.
*
* @param {*} value The value to check in the object.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.HasValueMatcher = function(value) {
/**
* @type {*}
* @private
*/
this.value_ = value;
};
/**
* Determines if an object contains a value
*
* @override
*/
goog.labs.testing.HasValueMatcher.prototype.matches =
function(actualObject) {
goog.asserts.assertObject(actualObject, 'Expected an Object');
var object = /** @type {!Object} */(actualObject);
return goog.object.containsValue(object, this.value_);
};
/**
* @override
*/
goog.labs.testing.HasValueMatcher.prototype.describe =
function(actualObject) {
return 'Input object did not contain the value: ' + this.value_;
};
/**
* Gives a matcher that asserts an object contains all the given key-value pairs
* in the input object.
*
* @param {!Object} entries The entries to check for presence in the object.
*
* @return {!goog.labs.testing.HasEntriesMatcher} A HasEntriesMatcher.
*/
function hasEntries(entries) {
return new goog.labs.testing.HasEntriesMatcher(entries);
}
/**
* Gives a matcher that asserts an object contains the given key-value pair.
*
* @param {string} key The key to check for presence in the object.
* @param {*} value The value to check for presence in the object.
*
* @return {!goog.labs.testing.HasEntryMatcher} A HasEntryMatcher.
*/
function hasEntry(key, value) {
return new goog.labs.testing.HasEntryMatcher(key, value);
}
/**
* Gives a matcher that asserts an object contains the given key.
*
* @param {string} key The key to check for presence in the object.
*
* @return {!goog.labs.testing.HasKeyMatcher} A HasKeyMatcher.
*/
function hasKey(key) {
return new goog.labs.testing.HasKeyMatcher(key);
}
/**
* Gives a matcher that asserts an object contains the given value.
*
* @param {*} value The value to check for presence in the object.
*
* @return {!goog.labs.testing.HasValueMatcher} A HasValueMatcher.
*/
function hasValue(value) {
return new goog.labs.testing.HasValueMatcher(value);
}
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - Object matchers
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.testing.dictionaryMatcherTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,65 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.testing.dictionaryMatcherTest');
goog.setTestOnly('goog.labs.testing.dictionaryMatcherTest');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.HasEntryMatcher');
goog.require('goog.labs.testing.MatcherError');
goog.require('goog.labs.testing.assertThat');
goog.require('goog.testing.jsunit');
function testHasEntries() {
var obj1 = {x: 1, y: 2, z: 3};
goog.labs.testing.assertThat(obj1, hasEntries({x: 1, y: 2}),
'obj1 has entries: {x:1, y:2}');
assertMatcherError(function() {
goog.labs.testing.assertThat(obj1, hasEntries({z: 5, a: 4}));
}, 'hasEntries should throw exception when it fails');
}
function testHasEntry() {
var obj1 = {x: 1, y: 2, z: 3};
goog.labs.testing.assertThat(obj1, hasEntry('x', 1),
'obj1 has entry: {x:1}');
assertMatcherError(function() {
goog.labs.testing.assertThat(obj1, hasEntry('z', 5));
}, 'hasEntry should throw exception when it fails');
}
function testHasKey() {
var obj1 = {x: 1};
goog.labs.testing.assertThat(obj1, hasKey('x'), 'obj1 has key x');
assertMatcherError(function() {
goog.labs.testing.assertThat(obj1, hasKey('z'));
}, 'hasKey should throw exception when it fails');
}
function testHasValue() {
var obj1 = {x: 1};
goog.labs.testing.assertThat(obj1, hasValue(1), 'obj1 has value 1');
assertMatcherError(function() {
goog.labs.testing.assertThat(obj1, hasValue(2));
}, 'hasValue should throw exception when it fails');
}
function assertMatcherError(callable, errorString) {
var e = assertThrows(errorString || 'callable throws exception', callable);
assertTrue(e instanceof goog.labs.testing.MatcherError);
}
@@ -0,0 +1,293 @@
// Copyright 2014 The Closure Library 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.
goog.provide('goog.labs.testing.Environment');
goog.require('goog.array');
goog.require('goog.debug.Console');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.jsunit');
/**
* JsUnit environments allow developers to customize the existing testing
* lifecycle by hitching additional setUp and tearDown behaviors to tests.
*
* Environments will run their setUp steps in the order in which they
* are instantiated and registered. During tearDown, the environments will
* unwind the setUp and execute in reverse order.
*
* See http://go/jsunit-env for more information.
*/
goog.labs.testing.Environment = goog.defineClass(null, {
/** @constructor */
constructor: function() {
goog.labs.testing.EnvironmentTestCase_.getInstance().
registerEnvironment_(this);
/** @type {goog.testing.MockControl} */
this.mockControl = null;
/** @type {goog.testing.MockClock} */
this.mockClock = null;
/** @private {boolean} */
this.shouldMakeMockControl_ = false;
/** @private {boolean} */
this.shouldMakeMockClock_ = false;
/** @const {!goog.debug.Console} */
this.console = goog.labs.testing.Environment.console_;
},
/** Runs immediately before the setUpPage phase of JsUnit tests. */
setUpPage: function() {
if (this.mockClock && this.mockClock.isDisposed()) {
this.mockClock = new goog.testing.MockClock(true);
}
},
/** Runs immediately after the tearDownPage phase of JsUnit tests. */
tearDownPage: function() {
// If we created the mockClock, we'll also dispose it.
if (this.shouldMakeMockClock_) {
this.mockClock.dispose();
}
},
/** Runs immediately before the setUp phase of JsUnit tests. */
setUp: goog.nullFunction,
/** Runs immediately after the tearDown phase of JsUnit tests. */
tearDown: function() {
// Make sure promises and other stuff that may still be scheduled, get a
// chance to run (and throw errors).
if (this.mockClock) {
for (var i = 0; i < 100; i++) {
this.mockClock.tick(1000);
}
// If we created the mockClock, we'll also reset it.
if (this.shouldMakeMockClock_) {
this.mockClock.reset();
}
}
// Make sure the user did not forget to call $replayAll & $verifyAll in
// their test. This is a noop if they did.
// This is important because:
// - Engineers thinks that not all their tests need to replay and verify.
// That lets tests sneak in that call mocks but never replay those calls.
// - Then some well meaning maintenance engineer wants to update the test
// with some new mock, adds a replayAll and BOOM the test fails
// because completely unrelated mocks now get replayed.
if (this.mockControl) {
try {
this.mockControl.$verifyAll();
this.mockControl.$replayAll();
this.mockControl.$verifyAll();
} finally {
this.mockControl.$resetAll();
}
if (this.shouldMakeMockControl_) {
// If we created the mockControl, we'll also tear it down.
this.mockControl.$tearDown();
}
}
// Verifying the mockControl may throw, so if cleanup needs to happen,
// add it further up in the function.
},
/**
* Create a new {@see goog.testing.MockControl} accessible via
* {@code env.mockControl} for each test. If your test has more than one
* testing environment, don't call this on more than one of them.
* @return {!goog.labs.testing.Environment} For chaining.
*/
withMockControl: function() {
if (!this.shouldMakeMockControl_) {
this.shouldMakeMockControl_ = true;
this.mockControl = new goog.testing.MockControl();
}
return this;
},
/**
* Create a {@see goog.testing.MockClock} for each test. The clock will be
* installed (override i.e. setTimeout) by default. It can be accessed
* using {@code env.mockClock}. If your test has more than one testing
* environment, don't call this on more than one of them.
* @return {!goog.labs.testing.Environment} For chaining.
*/
withMockClock: function() {
if (!this.shouldMakeMockClock_) {
this.shouldMakeMockClock_ = true;
this.mockClock = new goog.testing.MockClock(true);
}
return this;
},
/**
* Creates a basic strict mock of a {@code toMock}. For more advanced mocking,
* please use the MockControl directly.
* @param {Function} toMock
* @return {!goog.testing.StrictMock}
*/
mock: function(toMock) {
if (!this.shouldMakeMockControl_) {
throw new Error('MockControl not available on this environment. ' +
'Call withMockControl if this environment is expected ' +
'to contain a MockControl.');
}
return this.mockControl.createStrictMock(toMock);
}
});
/** @private @const {!goog.debug.Console} */
goog.labs.testing.Environment.console_ = new goog.debug.Console();
// Activate logging to the browser's console by default.
goog.labs.testing.Environment.console_.setCapturing(true);
/**
* An internal TestCase used to hook environments into the JsUnit test runner.
* Environments cannot be used in conjunction with custom TestCases for JsUnit.
* @private @final @constructor
* @extends {goog.testing.TestCase}
*/
goog.labs.testing.EnvironmentTestCase_ = function() {
goog.labs.testing.EnvironmentTestCase_.base(this, 'constructor');
/** @private {!Array<!goog.labs.testing.Environment>}> */
this.environments_ = [];
// Automatically install this TestCase when any environment is used in a test.
goog.testing.TestCase.initializeTestRunner(this);
};
goog.inherits(goog.labs.testing.EnvironmentTestCase_, goog.testing.TestCase);
goog.addSingletonGetter(goog.labs.testing.EnvironmentTestCase_);
/**
* Override the default global scope discovery of lifecycle functions to prevent
* overriding the custom environment setUp(Page)/tearDown(Page) logic.
* @override
*/
goog.labs.testing.EnvironmentTestCase_.prototype.autoDiscoverLifecycle =
function() {
if (goog.global['runTests']) {
this.runTests = goog.bind(goog.global['runTests'], goog.global);
}
if (goog.global['shouldRunTests']) {
this.shouldRunTests = goog.bind(goog.global['shouldRunTests'], goog.global);
}
};
/**
* Adds an environment to the JsUnit test.
* @param {!goog.labs.testing.Environment} env
* @private
*/
goog.labs.testing.EnvironmentTestCase_.prototype.registerEnvironment_ =
function(env) {
this.environments_.push(env);
};
/** @override */
goog.labs.testing.EnvironmentTestCase_.prototype.setUpPage = function() {
goog.array.forEach(this.environments_, function(env) {
env.setUpPage();
});
// User defined setUpPage method.
if (goog.global['setUpPage']) {
goog.global['setUpPage']();
}
};
/** @override */
goog.labs.testing.EnvironmentTestCase_.prototype.setUp = function() {
// User defined configure method.
if (goog.global['configureEnvironment']) {
goog.global['configureEnvironment']();
}
goog.array.forEach(this.environments_, function(env) {
env.setUp();
}, this);
// User defined setUp method.
if (goog.global['setUp']) {
goog.global['setUp']();
}
};
/** @override */
goog.labs.testing.EnvironmentTestCase_.prototype.tearDown = function() {
var firstException;
// User defined tearDown method.
if (goog.global['tearDown']) {
try {
goog.global['tearDown']();
} catch (e) {
if (!firstException) {
firstException = e;
}
}
}
// Execute the tearDown methods for the environment in the reverse order
// in which they were registered to "unfold" the setUp.
goog.array.forEachRight(this.environments_, function(env) {
// For tearDowns between tests make sure they run as much as possible to
// avoid interference between tests.
try {
env.tearDown();
} catch (e) {
if (!firstException) {
firstException = e;
}
}
});
if (firstException) {
throw firstException;
}
};
/** @override */
goog.labs.testing.EnvironmentTestCase_.prototype.tearDownPage = function() {
// User defined tearDownPage method.
if (goog.global['tearDownPage']) {
goog.global['tearDownPage']();
}
goog.array.forEachRight(this.environments_, function(env) {
env.tearDownPage();
});
};
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2014 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - JsUnit Environments
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.testing.environmentTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,210 @@
// Copyright 2014 The Closure Library 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.
goog.provide('goog.labs.testing.environmentTest');
goog.setTestOnly('goog.labs.testing.environmentTest');
goog.require('goog.labs.testing.Environment');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.jsunit');
var testCase = null;
var mockControl = null;
// Use this flag to control whether the global JsUnit lifecycle events are being
// called as part of the test lifecycle or as part of the "mocked" environment.
var testing = false;
function setUp() {
if (testing) {
return;
}
// Temporarily override the initializeTestRunner method to avoid installing
// our "test" TestCase.
var initFn = goog.testing.TestCase.initializeTestRunner;
goog.testing.TestCase.initializeTestRunner = function() {};
testCase = new goog.labs.testing.EnvironmentTestCase_();
goog.labs.testing.EnvironmentTestCase_.getInstance = function() {
return testCase;
};
goog.testing.TestCase.initializeTestRunner = initFn;
mockControl = new goog.testing.MockControl();
}
function tearDown() {
if (testing) {
return;
}
mockControl.$resetAll();
mockControl.$tearDown();
}
function testLifecycle() {
testing = true;
var envOne = mockControl.createStrictMock(goog.labs.testing.Environment);
var envTwo = mockControl.createStrictMock(goog.labs.testing.Environment);
var envThree = mockControl.createStrictMock(goog.labs.testing.Environment);
var testMethod = mockControl.createFunctionMock('testMethod');
testCase.addNewTest('testFake', testMethod);
testCase.registerEnvironment_(envOne);
testCase.registerEnvironment_(envTwo);
testCase.registerEnvironment_(envThree);
envOne.setUpPage();
envTwo.setUpPage();
envThree.setUpPage();
envOne.setUp();
envTwo.setUp();
envThree.setUp();
testMethod();
envThree.tearDown();
envTwo.tearDown();
envOne.tearDown();
envThree.tearDownPage();
envTwo.tearDownPage();
envOne.tearDownPage();
mockControl.$replayAll();
testCase.runTests();
mockControl.$verifyAll();
testing = false;
}
function testTearDownWithMockControl() {
testing = true;
var envWith = new goog.labs.testing.Environment();
var envWithout = new goog.labs.testing.Environment();
var mockControlMock = mockControl.createStrictMock(goog.testing.MockControl);
var mockControlCtorMock = mockControl.createMethodMock(goog.testing,
'MockControl');
mockControlCtorMock().$times(1).$returns(mockControlMock);
// Expecting verify / reset calls twice since two environments use the same
// mockControl, but only one created it and is allowed to tear it down.
mockControlMock.$verifyAll();
mockControlMock.$replayAll();
mockControlMock.$verifyAll();
mockControlMock.$resetAll();
mockControlMock.$tearDown().$times(1);
mockControlMock.$verifyAll();
mockControlMock.$replayAll();
mockControlMock.$verifyAll();
mockControlMock.$resetAll();
mockControl.$replayAll();
envWith.withMockControl();
envWithout.mockControl = mockControlMock;
envWith.tearDown();
envWithout.tearDown();
mockControl.$verifyAll();
mockControl.$resetAll();
testing = false;
}
function testAutoDiscoverTests() {
testing = true;
var setUpPageFn = testCase.setUpPage;
var setUpFn = testCase.setUp;
var tearDownFn = testCase.tearDownFn;
var tearDownPageFn = testCase.tearDownPageFn;
testCase.autoDiscoverTests();
assertEquals(setUpPageFn, testCase.setUpPage);
assertEquals(setUpFn, testCase.setUp);
assertEquals(tearDownFn, testCase.tearDownFn);
assertEquals(tearDownPageFn, testCase.tearDownPageFn);
// Note that this number changes when more tests are added to this file as
// the environment reflects on the window global scope for JsUnit.
assertEquals(6, testCase.tests_.length);
testing = false;
}
function testMockClock() {
testing = true;
var env = new goog.labs.testing.Environment().withMockClock();
testCase.addNewTest('testThatThrowsEventually', function() {
setTimeout(function() {
throw new Error('LateErrorMessage');
}, 200);
});
testCase.runTests();
assertTestFailure(testCase, 'testThatThrowsEventually', 'LateErrorMessage');
testing = false;
}
function testMockControl() {
testing = true;
var env = new goog.labs.testing.Environment().withMockControl();
var test = env.mockControl.createFunctionMock('test');
testCase.addNewTest('testWithoutVerify', function() {
test();
env.mockControl.$replayAll();
test();
});
testCase.runTests();
assertNull(env.mockClock);
testing = false;
}
function testMock() {
testing = true;
var env = new goog.labs.testing.Environment().withMockControl();
var mock = env.mock({
test: function() {}
});
testCase.addNewTest('testMockCalled', function() {
mock.test().$times(2);
env.mockControl.$replayAll();
mock.test();
mock.test();
env.mockControl.verifyAll();
});
testCase.runTests();
testing = false;
}
function assertTestFailure(testCase, name, message) {
assertContains(message, testCase.result_.resultsByName[name][0]);
}
@@ -0,0 +1,45 @@
// Copyright 2014 The Closure Library 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.
goog.provide('goog.labs.testing.environmentUsageTest');
goog.setTestOnly('goog.labs.testing.environmentUsageTest');
goog.require('goog.labs.testing.Environment');
var testing = false;
var env = new goog.labs.testing.Environment();
function setUpPage() {
assertFalse(testing);
}
function setUp() {
testing = true;
}
function testOne() {
assertTrue(testing);
}
function testTwo() {
assertTrue(testing);
}
function tearDown() {
testing = false;
}
function tearDownPage() {
assertFalse(testing);
}
@@ -0,0 +1,212 @@
// Copyright 2012 The Closure Library 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 Provides the built-in logic matchers: anyOf, allOf, and isNot.
*
*/
goog.provide('goog.labs.testing.AllOfMatcher');
goog.provide('goog.labs.testing.AnyOfMatcher');
goog.provide('goog.labs.testing.IsNotMatcher');
goog.require('goog.array');
goog.require('goog.labs.testing.Matcher');
/**
* The AllOf matcher.
*
* @param {!Array<!goog.labs.testing.Matcher>} matchers Input matchers.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.AllOfMatcher = function(matchers) {
/**
* @type {!Array<!goog.labs.testing.Matcher>}
* @private
*/
this.matchers_ = matchers;
};
/**
* Determines if all of the matchers match the input value.
*
* @override
*/
goog.labs.testing.AllOfMatcher.prototype.matches = function(actualValue) {
return goog.array.every(this.matchers_, function(matcher) {
return matcher.matches(actualValue);
});
};
/**
* Describes why the matcher failed. The returned string is a concatenation of
* all the failed matchers' error strings.
*
* @override
*/
goog.labs.testing.AllOfMatcher.prototype.describe =
function(actualValue) {
// TODO(user) : Optimize this to remove duplication with matches ?
var errorString = '';
goog.array.forEach(this.matchers_, function(matcher) {
if (!matcher.matches(actualValue)) {
errorString += matcher.describe(actualValue) + '\n';
}
});
return errorString;
};
/**
* The AnyOf matcher.
*
* @param {!Array<!goog.labs.testing.Matcher>} matchers Input matchers.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.AnyOfMatcher = function(matchers) {
/**
* @type {!Array<!goog.labs.testing.Matcher>}
* @private
*/
this.matchers_ = matchers;
};
/**
* Determines if any of the matchers matches the input value.
*
* @override
*/
goog.labs.testing.AnyOfMatcher.prototype.matches = function(actualValue) {
return goog.array.some(this.matchers_, function(matcher) {
return matcher.matches(actualValue);
});
};
/**
* Describes why the matcher failed.
*
* @override
*/
goog.labs.testing.AnyOfMatcher.prototype.describe =
function(actualValue) {
// TODO(user) : Optimize this to remove duplication with matches ?
var errorString = '';
goog.array.forEach(this.matchers_, function(matcher) {
if (!matcher.matches(actualValue)) {
errorString += matcher.describe(actualValue) + '\n';
}
});
return errorString;
};
/**
* The IsNot matcher.
*
* @param {!goog.labs.testing.Matcher} matcher The matcher to negate.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.IsNotMatcher = function(matcher) {
/**
* @type {!goog.labs.testing.Matcher}
* @private
*/
this.matcher_ = matcher;
};
/**
* Determines if the input value doesn't satisfy a matcher.
*
* @override
*/
goog.labs.testing.IsNotMatcher.prototype.matches = function(actualValue) {
return !this.matcher_.matches(actualValue);
};
/**
* Describes why the matcher failed.
*
* @override
*/
goog.labs.testing.IsNotMatcher.prototype.describe =
function(actualValue) {
return 'The following is false: ' + this.matcher_.describe(actualValue);
};
/**
* Creates a matcher that will succeed only if all of the given matchers
* succeed.
*
* @param {...goog.labs.testing.Matcher} var_args The matchers to test
* against.
*
* @return {!goog.labs.testing.AllOfMatcher} The AllOf matcher.
*/
function allOf(var_args) {
var matchers = goog.array.toArray(arguments);
return new goog.labs.testing.AllOfMatcher(matchers);
}
/**
* Accepts a set of matchers and returns a matcher which matches
* values which satisfy the constraints of any of the given matchers.
*
* @param {...goog.labs.testing.Matcher} var_args The matchers to test
* against.
*
* @return {!goog.labs.testing.AnyOfMatcher} The AnyOf matcher.
*/
function anyOf(var_args) {
var matchers = goog.array.toArray(arguments);
return new goog.labs.testing.AnyOfMatcher(matchers);
}
/**
* Returns a matcher that negates the input matcher. The returned
* matcher matches the values not matched by the input matcher and vice-versa.
*
* @param {!goog.labs.testing.Matcher} matcher The matcher to test against.
*
* @return {!goog.labs.testing.IsNotMatcher} The IsNot matcher.
*/
function isNot(matcher) {
return new goog.labs.testing.IsNotMatcher(matcher);
}
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - Logic matchers
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.testing.logicMatcherTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,57 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.testing.logicMatcherTest');
goog.setTestOnly('goog.labs.testing.logicMatcherTest');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.AllOfMatcher');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.GreaterThanMatcher');
goog.require('goog.labs.testing.MatcherError');
goog.require('goog.labs.testing.assertThat');
goog.require('goog.testing.jsunit');
function testAnyOf() {
goog.labs.testing.assertThat(5, anyOf(greaterThan(4), lessThan(3)),
'5 > 4 || 5 < 3');
goog.labs.testing.assertThat(2, anyOf(greaterThan(4), lessThan(3)),
'2 > 4 || 2 < 3');
assertMatcherError(function() {
goog.labs.testing.assertThat(4, anyOf(greaterThan(5), lessThan(2)));
}, 'anyOf should throw exception when it fails');
}
function testAllOf() {
goog.labs.testing.assertThat(5, allOf(greaterThan(4), lessThan(6)),
'5 > 4 && 5 < 6');
assertMatcherError(function() {
goog.labs.testing.assertThat(4, allOf(lessThan(5), lessThan(3)));
}, 'allOf should throw exception when it fails');
}
function testIsNot() {
goog.labs.testing.assertThat(5, isNot(greaterThan(6)), '5 !> 6');
assertMatcherError(function() {
goog.labs.testing.assertThat(4, isNot(greaterThan(3)));
}, 'isNot should throw exception when it fails');
}
function assertMatcherError(callable, errorString) {
var e = assertThrows(errorString || 'callable throws exception', callable);
assertTrue(e instanceof goog.labs.testing.MatcherError);
}
@@ -0,0 +1,80 @@
// Copyright 2012 The Closure Library 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 Provides the base Matcher interface. User code should use the
* matchers through assertThat statements and not directly.
*/
goog.provide('goog.labs.testing.Matcher');
/**
* A matcher object to be used in assertThat statements.
* @interface
*/
goog.labs.testing.Matcher = function() {};
/**
* Determines whether a value matches the constraints of the match.
*
* @param {*} value The object to match.
* @return {boolean} Whether the input value matches this matcher.
*/
goog.labs.testing.Matcher.prototype.matches = function(value) {};
/**
* Describes why the matcher failed.
*
* @param {*} value The value that didn't match.
* @param {string=} opt_description A partial description to which the reason
* will be appended.
*
* @return {string} Description of why the matcher failed.
*/
goog.labs.testing.Matcher.prototype.describe =
function(value, opt_description) {};
/**
* Generates a Matcher from the matches and describe functions passed in.
*
* @param {!Function} matchesFunction The matches function.
* @param {Function=} opt_describeFunction The describe function.
* @return {!Function} The custom matcher.
*/
goog.labs.testing.Matcher.makeMatcher =
function(matchesFunction, opt_describeFunction) {
/**
* @constructor
* @implements {goog.labs.testing.Matcher}
* @final
*/
var matcherConstructor = function() {};
/** @override */
matcherConstructor.prototype.matches = matchesFunction;
if (opt_describeFunction) {
/** @override */
matcherConstructor.prototype.describe = opt_describeFunction;
}
return matcherConstructor;
};
@@ -0,0 +1,346 @@
// Copyright 2012 The Closure Library 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 Provides the built-in number matchers like lessThan,
* greaterThan, etc.
*/
goog.provide('goog.labs.testing.CloseToMatcher');
goog.provide('goog.labs.testing.EqualToMatcher');
goog.provide('goog.labs.testing.GreaterThanEqualToMatcher');
goog.provide('goog.labs.testing.GreaterThanMatcher');
goog.provide('goog.labs.testing.LessThanEqualToMatcher');
goog.provide('goog.labs.testing.LessThanMatcher');
goog.require('goog.asserts');
goog.require('goog.labs.testing.Matcher');
/**
* The GreaterThan matcher.
*
* @param {number} value The value to compare.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.GreaterThanMatcher = function(value) {
/**
* @type {number}
* @private
*/
this.value_ = value;
};
/**
* Determines if input value is greater than the expected value.
*
* @override
*/
goog.labs.testing.GreaterThanMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue > this.value_;
};
/**
* @override
*/
goog.labs.testing.GreaterThanMatcher.prototype.describe =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue + ' is not greater than ' + this.value_;
};
/**
* The lessThan matcher.
*
* @param {number} value The value to compare.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.LessThanMatcher = function(value) {
/**
* @type {number}
* @private
*/
this.value_ = value;
};
/**
* Determines if the input value is less than the expected value.
*
* @override
*/
goog.labs.testing.LessThanMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue < this.value_;
};
/**
* @override
*/
goog.labs.testing.LessThanMatcher.prototype.describe =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue + ' is not less than ' + this.value_;
};
/**
* The GreaterThanEqualTo matcher.
*
* @param {number} value The value to compare.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.GreaterThanEqualToMatcher = function(value) {
/**
* @type {number}
* @private
*/
this.value_ = value;
};
/**
* Determines if the input value is greater than equal to the expected value.
*
* @override
*/
goog.labs.testing.GreaterThanEqualToMatcher.prototype.matches =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue >= this.value_;
};
/**
* @override
*/
goog.labs.testing.GreaterThanEqualToMatcher.prototype.describe =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue + ' is not greater than equal to ' + this.value_;
};
/**
* The LessThanEqualTo matcher.
*
* @param {number} value The value to compare.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.LessThanEqualToMatcher = function(value) {
/**
* @type {number}
* @private
*/
this.value_ = value;
};
/**
* Determines if the input value is less than or equal to the expected value.
*
* @override
*/
goog.labs.testing.LessThanEqualToMatcher.prototype.matches =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue <= this.value_;
};
/**
* @override
*/
goog.labs.testing.LessThanEqualToMatcher.prototype.describe =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue + ' is not less than equal to ' + this.value_;
};
/**
* The EqualTo matcher.
*
* @param {number} value The value to compare.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.EqualToMatcher = function(value) {
/**
* @type {number}
* @private
*/
this.value_ = value;
};
/**
* Determines if the input value is equal to the expected value.
*
* @override
*/
goog.labs.testing.EqualToMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue === this.value_;
};
/**
* @override
*/
goog.labs.testing.EqualToMatcher.prototype.describe =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue + ' is not equal to ' + this.value_;
};
/**
* The CloseTo matcher.
*
* @param {number} value The value to compare.
* @param {number} range The range to check within.
*
* @constructor
* @struct
* @implements {goog.labs.testing.Matcher}
* @final
*/
goog.labs.testing.CloseToMatcher = function(value, range) {
/**
* @type {number}
* @private
*/
this.value_ = value;
/**
* @type {number}
* @private
*/
this.range_ = range;
};
/**
* Determines if input value is within a certain range of the expected value.
*
* @override
*/
goog.labs.testing.CloseToMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertNumber(actualValue);
return Math.abs(this.value_ - actualValue) < this.range_;
};
/**
* @override
*/
goog.labs.testing.CloseToMatcher.prototype.describe =
function(actualValue) {
goog.asserts.assertNumber(actualValue);
return actualValue + ' is not close to(' + this.range_ + ') ' + this.value_;
};
/**
* @param {number} value The expected value.
*
* @return {!goog.labs.testing.GreaterThanMatcher} A GreaterThanMatcher.
*/
function greaterThan(value) {
return new goog.labs.testing.GreaterThanMatcher(value);
}
/**
* @param {number} value The expected value.
*
* @return {!goog.labs.testing.GreaterThanEqualToMatcher} A
* GreaterThanEqualToMatcher.
*/
function greaterThanEqualTo(value) {
return new goog.labs.testing.GreaterThanEqualToMatcher(value);
}
/**
* @param {number} value The expected value.
*
* @return {!goog.labs.testing.LessThanMatcher} A LessThanMatcher.
*/
function lessThan(value) {
return new goog.labs.testing.LessThanMatcher(value);
}
/**
* @param {number} value The expected value.
*
* @return {!goog.labs.testing.LessThanEqualToMatcher} A LessThanEqualToMatcher.
*/
function lessThanEqualTo(value) {
return new goog.labs.testing.LessThanEqualToMatcher(value);
}
/**
* @param {number} value The expected value.
*
* @return {!goog.labs.testing.EqualToMatcher} An EqualToMatcher.
*/
function equalTo(value) {
return new goog.labs.testing.EqualToMatcher(value);
}
/**
* @param {number} value The expected value.
* @param {number} range The maximum allowed difference from the expected value.
*
* @return {!goog.labs.testing.CloseToMatcher} A CloseToMatcher.
*/
function closeTo(value, range) {
return new goog.labs.testing.CloseToMatcher(value, range);
}
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - Number matchers
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.labs.testing.numberMatcherTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,71 @@
// Copyright 2012 The Closure Library 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.
goog.provide('goog.labs.testing.numberMatcherTest');
goog.setTestOnly('goog.labs.testing.numberMatcherTest');
/** @suppress {extraRequire} */
goog.require('goog.labs.testing.LessThanMatcher');
goog.require('goog.labs.testing.MatcherError');
goog.require('goog.labs.testing.assertThat');
goog.require('goog.testing.jsunit');
function testGreaterThan() {
goog.labs.testing.assertThat(4, greaterThan(3), '4 > 3');
assertMatcherError(function() {
goog.labs.testing.assertThat(2, greaterThan(3));
}, '2 > 3');
}
function testGreaterThanEqualTo() {
goog.labs.testing.assertThat(5, greaterThanEqualTo(4), '5 >= 4');
goog.labs.testing.assertThat(5, greaterThanEqualTo(5), '5 >= 5');
assertMatcherError(function() {
goog.labs.testing.assertThat(3, greaterThanEqualTo(5));
}, '3 >= 5');
}
function testLessThan() {
goog.labs.testing.assertThat(6, lessThan(7), '6 < 7');
assertMatcherError(function() {
goog.labs.testing.assertThat(7, lessThan(5));
}, '7 < 5');
}
function testLessThanEqualTo() {
goog.labs.testing.assertThat(8, lessThanEqualTo(8), '8 <= 8');
goog.labs.testing.assertThat(8, lessThanEqualTo(9), '8 <= 9');
assertMatcherError(function() {
goog.labs.testing.assertThat(7, lessThanEqualTo(5));
}, '7 <= 5');
}
function testEqualTo() {
goog.labs.testing.assertThat(7, equalTo(7), '7 equals 7');
assertMatcherError(function() {
goog.labs.testing.assertThat(7, equalTo(5));
}, '7 == 5');
}
function testCloseTo() {
goog.labs.testing.assertThat(7, closeTo(10, 4), '7 within range(4) of 10');
assertMatcherError(function() {
goog.labs.testing.assertThat(5, closeTo(10, 3));
}, '5 within range(3) of 10');
}
function assertMatcherError(callable, errorString) {
var e = assertThrows(errorString || 'callable throws exception', callable);
assertTrue(e instanceof goog.labs.testing.MatcherError);
}

Some files were not shown because too many files have changed in this diff Show More