Adding mapbox-gl branch
This commit is contained in:
@@ -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));
|
||||
}
|
||||
+22
@@ -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>
|
||||
+78
@@ -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><<i>element</i> <i>attr</i>="..."></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><>!</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<?...?>>',
|
||||
'<script>alert(1337)</script>',
|
||||
'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&hl=en">Click</a>',
|
||||
'URL normalized');
|
||||
}
|
||||
|
||||
function testNaiveAttributeRewriterCaught() {
|
||||
run('<a href="javascript:http://google.com/ 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, "Nevermore"">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 & Bar & Baz">/</I>',
|
||||
'<i title="Foo & Bar & Baz">/</i>',
|
||||
'attr value not over-escaped');
|
||||
}
|
||||
|
||||
function testTagSpecificRulesTakePrecedence() {
|
||||
run('<a title=zogberts>Link</a>',
|
||||
'<a title="<zogberts>">Link</a>',
|
||||
'tag specific rules take precedence');
|
||||
}
|
||||
|
||||
function testAttributeRejectionLocalized() {
|
||||
run('<a id=foo href =//evil.org/ title=>Link</a>',
|
||||
'<a title="<>">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><>!</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<?...?>>',
|
||||
'<script>alert(1337)</script>',
|
||||
'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&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, "Nevermore"">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 & Bar & Baz">/</I>',
|
||||
'<i title="Foo & Bar & Baz">/</i>',
|
||||
'attr value not over-escaped');
|
||||
}
|
||||
|
||||
function testTagSpecificRulesTakePrecedence() {
|
||||
run('<a title=zogberts>Link</a>',
|
||||
'<a title="<zogberts>">Link</a>',
|
||||
'tag specific rules take precedence');
|
||||
}
|
||||
|
||||
function testAttributeRejectionLocalized() {
|
||||
run('<a id=foo href =//evil.org/ title=>Link</a>',
|
||||
'<a title="<>">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;
|
||||
};
|
||||
+278
@@ -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
|
||||
+22
@@ -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>
|
||||
+119
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
+376
@@ -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
|
||||
+22
@@ -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>
|
||||
+254
@@ -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
|
||||
+22
@@ -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
Reference in New Issue
Block a user