Update wmts-hidpi, add nicer-api-docs

This commit is contained in:
Andreas Hocevar
2014-05-06 13:02:46 -05:00
parent b3ac1afd00
commit 1e25fc5585
2239 changed files with 3726515 additions and 37010 deletions

View File

@@ -0,0 +1,162 @@
// 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 Restricted class definitions for Closure.
*
* @author johnlenz@google.com (John Lenz)
*/
goog.provide('goog.labs.classdef');
/** @typedef {
{constructor:!Function}|
{constructor:!Function, statics:(Object|function(Function):void)}} */
goog.labs.classdef.ClassDescriptor;
/**
* Creates a restricted form of a Closure "class":
* - from the compiler's perspective, the instance returned from the
* constructor is sealed (no new properties may be added). This enables
* better type checking.
* - the compiler will rewrite this definition to a form that is optimal
* for type checking and optimization (initially this will be a more
* traditional form).
*
* @param {Function} superClass The superclass or null.
* @param {goog.labs.classdef.ClassDescriptor} def
* An object literal describing the
* the class. It may have the following properties:
* "constructor": the constructor function
* "statics": an object literal containing methods to add to the constructor
* as "static" methods or a function that will recieve the constructor
* function as its only parameter to which static properties can
* be added.
* all other properties are added to the prototype.
* @return {!Function} The class constructor.
*/
goog.labs.classdef.defineClass = function(superClass, def) {
// TODO(johnlenz): consider making the superClass an optional parameter.
var constructor = def.constructor;
var statics = def.statics;
// Wrap the constructor prior to setting up the prototype and static methods.
if (!constructor || constructor == Object.prototype.constructor) {
throw Error('constructor property is required.');
}
var cls = goog.labs.classdef.createSealingConstructor_(constructor);
if (superClass) {
goog.inherits(cls, superClass);
}
// Remove all the properties that should not be copied to the prototype.
delete def.constructor;
delete def.statics;
goog.labs.classdef.applyProperties_(cls.prototype, def);
if (statics != null) {
if (statics instanceof Function) {
statics(cls);
} else {
goog.labs.classdef.applyProperties_(cls, statics);
}
}
return cls;
};
/**
* @define {boolean} Whether the instances returned by
* goog.labs.classdef.defineClass should be sealed when possible.
*/
goog.define('goog.labs.classdef.SEAL_CLASS_INSTANCES', goog.DEBUG);
/**
* If goog.labs.classdef.SEAL_CLASS_INSTANCES is enabled and Object.seal is
* defined, this function will wrap the constructor in a function that seals the
* results of the provided constructor function.
*
* @param {!Function} ctr The constructor whose results maybe be sealed.
* @return {!Function} The replacement constructor.
* @private
*/
goog.labs.classdef.createSealingConstructor_ = function(ctr) {
if (goog.labs.classdef.SEAL_CLASS_INSTANCES &&
Object.seal instanceof Function) {
/** @this {*} */
var wrappedCtr = function() {
// Don't seal an instance of a subclass when it calls the constructor of
// its super class as there is most likely still setup to do.
var instance = ctr.apply(this, arguments) || this;
if (this.constructor === wrappedCtr) {
Object.seal(instance);
}
return instance;
};
return wrappedCtr;
}
return ctr;
};
// TODO(johnlenz): share these values with the goog.object
/**
* The names of the fields that are defined on Object.prototype.
* @type {!Array.<string>}
* @private
* @const
*/
goog.labs.classdef.OBJECT_PROTOTYPE_FIELDS_ = [
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf'
];
// TODO(johnlenz): share this function with the goog.object
/**
* @param {!Object} target The object to add properties to.
* @param {!Object} source The object to copy properites from.
* @private
*/
goog.labs.classdef.applyProperties_ = function(target, source) {
// TODO(johnlenz): update this to support ES5 getters/setters
var key;
for (key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
// For IE the for-in-loop does not contain any properties that are not
// enumerable on the prototype object (for example isPrototypeOf from
// Object.prototype) and it will also not include 'replace' on objects that
// extend String and change 'replace' (not that it is common for anyone to
// extend anything except Object).
for (var i = 0; i < goog.labs.classdef.OBJECT_PROTOTYPE_FIELDS_.length; i++) {
key = goog.labs.classdef.OBJECT_PROTOTYPE_FIELDS_[i];
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
};

View File

@@ -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']
};
};

View File

@@ -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);
}

View File

@@ -0,0 +1,391 @@
// 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.
*
*/
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}
*/
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.base(this, 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.
* @return {!Array.<!Array.<string>>} The parsed CSV.
*/
goog.labs.format.csv.parse = function(text) {
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;
}
throw new goog.labs.format.csv.ParseError(
text, index - 1,
'Unexpected character "' + token + '" after quote mark');
}
}
if (goog.isNull(end)) {
throw new goog.labs.format.csv.ParseError(
text, text.length - 1,
'Unexpected end of text after open quote');
}
// 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 == '"') {
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.');
}
};

View File

@@ -0,0 +1,169 @@
// 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 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_({});
});
}

View File

@@ -0,0 +1,733 @@
// 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.require('goog.array');
goog.require('goog.debug');
goog.require('goog.debug.Error');
goog.require('goog.functions');
goog.require('goog.json');
/**
* 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.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 {
return goog.json.serialize(arg);
}
});
return methodName + '(' + opt_args.join(', ') + ')';
};
/**
* Error thrown when verification failed.
*
* @param {Array} 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}
*/
goog.labs.mock.VerificationError = function(recordedCalls, methodName, args) {
var msg = goog.labs.mock.VerificationError.getVerificationErrorMsg_(
recordedCalls, methodName, args);
goog.base(this, 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} 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
* @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.
*
* @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.findBinding =
function(methodName, args) {
var stub = goog.array.find(this.methodBindings, function(binding) {
return binding.matches(methodName, args, false /* isVerification */);
});
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.findBinding(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
* @extends {goog.labs.mock.MockManager_}
* @private
*/
goog.labs.mock.MockObjectManager_ = function(objOrClass) {
goog.base(this);
/**
* 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 */
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 */
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
* @extends {goog.labs.mock.MockObjectManager_}
* @private
*/
goog.labs.mock.MockSpyManager_ = function(obj) {
goog.base(this, 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.findBinding =
function(methodName, args) {
var stub = goog.base(this, 'findBinding', 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
* @extends {goog.labs.mock.MockManager_}
* @private
*/
goog.labs.mock.MockFunctionManager_ = function(func) {
goog.base(this);
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
* @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
* @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);
}
});
};

View File

@@ -0,0 +1,93 @@
// 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.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.net.EventType');
goog.require('goog.result.SimpleResult');
goog.require('goog.userAgent');
/**
* Loads a single image. Useful for preloading images. May be combined with
* goog.result.combine to preload many 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.result.Result} An asyncronous result that will succeed
* if the image successfully loads or error if the image load fails.
*/
goog.labs.net.image.load = function(uri, opt_image) {
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. Instead, we 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
var loadEvent = goog.userAgent.IE ? goog.net.EventType.READY_STATE_CHANGE :
goog.events.EventType.LOAD;
var result = new goog.result.SimpleResult();
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) {
result.setValue(image);
} else {
result.setError();
}
});
// Initiate the image request.
image.src = uri;
return result;
};

View File

@@ -0,0 +1,128 @@
// 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.events');
goog.require('goog.labs.net.image');
goog.require('goog.result');
goog.require('goog.result.Result');
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');
assertEquals(0, goog.events.getTotalListenerCount());
var result = goog.labs.net.image.load(url);
goog.result.waitOnSuccess(result, function(value) {
assertEquals(goog.result.Result.State.SUCCESS, result.getState());
assertEquals('IMG', value.tagName);
assertTrue(goog.string.endsWith(value.src, url));
assertUndefined(result.getError());
assertEquals('Listeners should have been cleaned up.',
0, goog.events.getTotalListenerCount());
asyncTestCase.continueTesting();
});
}
function testInvalidImage() {
var url = 'testdata/invalid.gif'; // This file does not exist.
asyncTestCase.waitForAsync('image load');
assertEquals(0, goog.events.getTotalListenerCount());
var result = goog.labs.net.image.load(url);
goog.result.wait(result, function(result) {
assertEquals(goog.result.Result.State.ERROR, result.getState());
assertUndefined(result.getValue());
assertUndefined(result.getError());
assertEquals('Listeners should have been cleaned up.',
0, goog.events.getTotalListenerCount());
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');
assertEquals(0, goog.events.getTotalListenerCount());
var result = goog.labs.net.image.load(url, countedFactory);
goog.result.waitOnSuccess(result, function(value) {
assertEquals(goog.result.Result.State.SUCCESS, result.getState());
assertEquals(returnedImage, value);
assertEquals(1, countedFactory.getCallCount());
assertUndefined(result.getError());
assertEquals('Listeners should have been cleaned up.',
0, goog.events.getTotalListenerCount());
asyncTestCase.continueTesting();
});
}
function testExistingImage() {
var image = new Image();
var url = 'testdata/cleardot.gif';
asyncTestCase.waitForAsync('image load');
assertEquals(0, goog.events.getTotalListenerCount());
var result = goog.labs.net.image.load(url, image);
goog.result.waitOnSuccess(result, function(value) {
assertEquals(goog.result.Result.State.SUCCESS, result.getState());
assertEquals(image, value);
assertUndefined(result.getError());
assertEquals('Listeners should have been cleaned up.',
0, goog.events.getTotalListenerCount());
asyncTestCase.continueTesting();
});
}

View File

@@ -0,0 +1,188 @@
// 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.
*
*/
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 of a WebChannel (TODO(user): to complete):
* 1) delivery: ordered, reliable, timeout
* 2) HTTP: special headers, URI prefix, cross domains
* 3) debugging: stats, logging
* 4) pattern: full-duplex, server-client or client-server messaging
* 5) QoS: priority, throttling,
* 6) buffer management: batch size, delivery interval
*
* 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}.
*
* @typedef {{
* ordered: (boolean|undefined),
* reliable: (boolean|undefined),
* timeoutMs: (number|undefined),
* priority: (number|undefined)
* }}
*/
goog.net.WebChannel.Options;
/**
* Types that are allowed as message data.
*
* @typedef {(ArrayBuffer|Blob|Object.<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.base(this, 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.base(this, 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;

View File

@@ -0,0 +1,639 @@
// 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.json.EvalJsonProcessor');
goog.require('goog.labs.net.webChannel.Channel');
goog.require('goog.labs.net.webChannel.WebChannelRequest');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.labs.net.webChannel.requestStats.ServerReachability');
goog.require('goog.labs.net.webChannel.requestStats.Stat');
goog.require('goog.net.tmpnetwork');
/**
* 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. It also runs the logic to see if the channel
* has been blocked by a network administrator.
*
* @constructor
* @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
* @type {!goog.labs.net.webChannel.Channel}
* @private
*/
this.channel_ = channel;
/**
* The channel debug to use for logging
* @type {!goog.labs.net.webChannel.WebChannelDebug}
* @private
*/
this.channelDebug_ = channelDebug;
/**
* Parser for a response payload. Defaults to use
* {@code goog.json.unsafeParse}. The parser should return an array.
* @type {goog.string.Parser}
* @private
*/
this.parser_ = new goog.json.EvalJsonProcessor(null, true);
};
goog.scope(function() {
var BaseTestChannel = goog.labs.net.webChannel.BaseTestChannel;
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
var WebChannelRequest = goog.labs.net.webChannel.WebChannelRequest;
var requestStats = goog.labs.net.webChannel.requestStats;
var Channel = goog.labs.net.webChannel.Channel;
/**
* Extra HTTP headers to add to all the requests sent to the server.
* @type {Object}
* @private
*/
BaseTestChannel.prototype.extraHeaders_ = null;
/**
* The test request.
* @type {WebChannelRequest}
* @private
*/
BaseTestChannel.prototype.request_ = null;
/**
* Whether we have received the first result as an intermediate result. This
* helps us determine whether we're behind a buffering proxy.
* @type {boolean}
* @private
*/
BaseTestChannel.prototype.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.
* @type {?number}
* @private
*/
BaseTestChannel.prototype.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.
* @type {?number}
* @private
*/
BaseTestChannel.prototype.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.
* @type {?number}
* @private
*/
BaseTestChannel.prototype.lastTime_ = null;
/**
* The relative path for test requests.
* @type {?string}
* @private
*/
BaseTestChannel.prototype.path_ = null;
/**
* The state of the state machine for this object.
*
* @type {?number}
* @private
*/
BaseTestChannel.prototype.state_ = null;
/**
* The last status code received.
* @type {number}
* @private
*/
BaseTestChannel.prototype.lastStatusCode_ = -1;
/**
* A subdomain prefix for using a subdomain in IE for the backchannel
* requests.
* @type {?string}
* @private
*/
BaseTestChannel.prototype.hostPrefix_ = null;
/**
* A subdomain prefix for testing whether the channel was disabled by
* a network administrator;
* @type {?string}
* @private
*/
BaseTestChannel.prototype.blockedPrefix_ = null;
/**
* 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
* see if the channel has been blocked.
*/
CHECKING_BLOCKED: 1,
/**
* The state for the TestChannel state machine where we're checking to
* se if we're behind a buffering proxy.
*/
CONNECTION_TESTING: 2
};
/**
* Time in MS for waiting for the request to see if the channel is blocked.
* If the response takes longer than this many ms, we assume the request has
* failed.
* @type {number}
* @private
*/
BaseTestChannel.BLOCKED_TIMEOUT_ = 5000;
/**
* Number of attempts to try to see if the check to see if we're blocked
* succeeds. Sometimes the request can fail because of flaky network conditions
* and checking multiple times reduces false positives.
* @type {number}
* @private
*/
BaseTestChannel.BLOCKED_RETRIES_ = 3;
/**
* Time in ms between retries of the blocked request
* @type {number}
* @private
*/
BaseTestChannel.BLOCKED_PAUSE_BETWEEN_RETRIES_ = 2000;
/**
* 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;
};
/**
* Sets a new parser for the response payload. A custom parser may be set to
* avoid using eval(), for example.
* By default, the parser uses {@code goog.json.unsafeParse}.
* @param {!goog.string.Parser} parser Parser.
*/
BaseTestChannel.prototype.setParser = function(parser) {
this.parser_ = parser;
};
/**
* 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 first test, then skip it.
var firstTestResults = this.channel_.getFirstTestResults();
if (goog.isDefAndNotNull(firstTestResults)) {
this.hostPrefix_ = this.channel_.correctHostPrefix(firstTestResults[0]);
this.blockedPrefix_ = firstTestResults[1];
if (this.blockedPrefix_) {
this.state_ = BaseTestChannel.State_.CHECKING_BLOCKED;
this.checkBlocked_();
} else {
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.connectStage2_();
}
return;
}
// the first request returns server specific parameters
sendDataUri.setParameterValues('MODE', 'init');
this.request_ = WebChannelRequest.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;
};
/**
* Checks to see whether the channel is blocked. This is for implementing the
* feature that allows network administrators to block Gmail Chat. The
* strategy to determine if we're blocked is to try to load an image off a
* special subdomain that network administrators will block access to if they
* are trying to block chat. For Gmail Chat, the subdomain is
* chatenabled.mail.google.com.
* @private
*/
BaseTestChannel.prototype.checkBlocked_ = function() {
var uri = this.channel_.createDataUri(this.blockedPrefix_,
'/mail/images/cleardot.gif');
uri.makeUnique();
goog.net.tmpnetwork.testLoadImageWithRetries(uri.toString(),
BaseTestChannel.BLOCKED_TIMEOUT_,
goog.bind(this.checkBlockedCallback_, this),
BaseTestChannel.BLOCKED_RETRIES_,
BaseTestChannel.BLOCKED_PAUSE_BETWEEN_RETRIES_);
requestStats.notifyServerReachabilityEvent(
requestStats.ServerReachability.REQUEST_MADE);
};
/**
* Callback for testLoadImageWithRetries to check if a channel is blocked.
* @param {boolean} succeeded Whether the request succeeded.
* @private
*/
BaseTestChannel.prototype.checkBlockedCallback_ = function(
succeeded) {
if (succeeded) {
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.connectStage2_();
} else {
requestStats.notifyStatEvent(requestStats.Stat.CHANNEL_BLOCKED);
this.channel_.testConnectionBlocked(this);
}
// We don't dispatch a REQUEST_FAILED server reachability event when the
// block request fails, as such a failure is not a good signal that the
// server has actually become unreachable.
if (succeeded) {
requestStats.notifyServerReachabilityEvent(
requestStats.ServerReachability.REQUEST_SUCCEEDED);
}
};
/**
* 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.connectStage2_ = function() {
this.channelDebug_.debug('TestConnection: starting stage 2');
// If the second test results are available, skip its execution.
var secondTestResults = this.channel_.getSecondTestResults();
if (goog.isDefAndNotNull(secondTestResults)) {
this.channelDebug_.debug(
'TestConnection: skipping stage 2, precomputed result is ' +
secondTestResults ? 'Buffered' : 'Unbuffered');
requestStats.notifyStatEvent(requestStats.Stat.TEST_STAGE_TWO_START);
if (secondTestResults) { // 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_ = WebChannelRequest.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 (!WebChannelRequest.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 {WebChannelRequest} 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,
WebChannelRequest.Error.BAD_DATA);
return;
}
/** @preserveTry */
try {
var respArray = this.parser_.parse(responseText);
} catch (e) {
this.channelDebug_.dumpException(e);
this.channel_.testConnectionFailure(this,
WebChannelRequest.Error.BAD_DATA);
return;
}
this.hostPrefix_ = this.channel_.correctHostPrefix(respArray[0]);
this.blockedPrefix_ = respArray[1];
} 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 {WebChannelRequest} 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 {WebChannelRequest.Error} */
(this.request_.getLastError()));
return;
}
if (this.state_ == BaseTestChannel.State_.INIT) {
this.channelDebug_.debug(
'TestConnection: request complete for initial check');
if (this.blockedPrefix_) {
this.state_ = BaseTestChannel.State_.CHECKING_BLOCKED;
this.checkBlocked_();
} else {
this.state_ = BaseTestChannel.State_.CONNECTION_TESTING;
this.connectStage2_();
}
} else if (this.state_ == BaseTestChannel.State_.CONNECTION_TESTING) {
this.channelDebug_.debug('TestConnection: request complete for stage 2');
var goodConn = false;
if (!WebChannelRequest.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);
}
}
};
/**
* 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 WebChannelRequest.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.testConnectionBlocked = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFinished = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* @override
*/
BaseTestChannel.prototype.getFirstTestResults = goog.abstractMethod;
}); // goog.scope

View File

@@ -0,0 +1,195 @@
// 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 {//visibility:private}
*/
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.WebChannelRequest} 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.WebChannelRequest} 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 blocked.
* @param {goog.labs.net.webChannel.BaseTestChannel} testChannel
* The TestChannel.
*/
Channel.prototype.testConnectionBlocked = 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.WebChannelRequest.Error} errorCode
* The error code of the failure.
*/
Channel.prototype.testConnectionFailure = goog.abstractMethod;
/**
* Not needed for testchannel.
* Gets the results for the first channel test
* @return {Array.<string>} The results.
*/
Channel.prototype.getFirstTestResults = goog.abstractMethod;
}); // goog.scope

View File

@@ -0,0 +1,391 @@
// 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
* WebChannelRequest.
*
* @visibility {//visibility:private}
*/
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.
* @type {Object}
*/
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.
* TODO(user) - Change name not to use Event or use EventTarget
* @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 channel is blocked by a network administrator. */
CHANNEL_BLOCKED: 4,
/** Event indicating the start of test stage two. */
TEST_STAGE_TWO_START: 5,
/** Event indicating the first piece of test data was received. */
TEST_STAGE_TWO_DATA_ONE: 6,
/**
* Event indicating that the second piece of test data was received and it was
* recieved separately from the first.
*/
TEST_STAGE_TWO_DATA_TWO: 7,
/** Event indicating both pieces of test data were received simultaneously. */
TEST_STAGE_TWO_DATA_BOTH: 8,
/** Event indicating stage one of the test request failed. */
TEST_STAGE_ONE_FAILED: 9,
/** Event indicating stage two of the test request failed. */
TEST_STAGE_TWO_FAILED: 10,
/**
* Event indicating that a buffering proxy is likely between the client and
* the server.
*/
PROXY: 11,
/**
* Event indicating that no buffering proxy is likely between the client and
* the server.
*/
NOPROXY: 12,
/** Event indicating an unknown SID error. */
REQUEST_UNKNOWN_SESSION_ID: 13,
/** Event indicating a bad status code was received. */
REQUEST_BAD_STATUS: 14,
/** Event indicating incomplete data was received */
REQUEST_INCOMPLETE_DATA: 15,
/** Event indicating bad data was received */
REQUEST_BAD_DATA: 16,
/** Event indicating no data was received when data was expected. */
REQUEST_NO_DATA: 17,
/** Event indicating a request timeout. */
REQUEST_TIMEOUT: 18,
/**
* Event indicating that the server never received our hanging GET and so it
* is being retried.
*/
BACKCHANNEL_MISSING: 19,
/**
* 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: 20,
/**
* The browser declared itself offline during the lifetime of a request, or
* was offline when a request was initially made.
*/
BROWSER_OFFLINE: 21,
/** ActiveX is blocked by the machine's admin settings. */
ACTIVE_X_BLOCKED: 22
};
/**
* 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

View File

@@ -0,0 +1,274 @@
// 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.string.path');
/**
* Implementation of {@link goog.net.WebChannelTransport} with
* {@link goog.labs.net.webChannel.WebChannelBase} as the underlying channel
* implementation.
*
* @constructor
* @implements {goog.net.WebChannelTransport}
*/
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}
*/
WebChannelBaseTransport.Channel = function(url, opt_options) {
goog.base(this);
/**
* The underlying channel object.
*
* @type {!WebChannelBase}
* @private
*/
this.channel_ = new WebChannelBase();
/**
* The URL of the target server end-point.
*
* @type {string}
* @private
*/
this.url_ = url;
/**
* The channel options.
*
* @type {?goog.net.WebChannel.Options}
* @private
*/
this.options_ = opt_options || null;
/**
* The logger for this class.
* @type {goog.log.Logger}
* @private
*/
this.logger_ = goog.log.getLogger(
'goog.labs.net.webChannel.WebChannelBaseTransport');
};
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".
*
* TODO(user): The test path may be made configurable via the options.
*
* @override
*/
WebChannelBaseTransport.Channel.prototype.open = function() {
var testUrl = goog.string.path.join(this.url_, 'test');
this.channel_.connect(testUrl, this.url_);
this.channelHandler_ = new WebChannelBaseTransport.Channel.Handler_(this);
this.channel_.setHandler(this.channelHandler_);
};
/**
* @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_;
goog.base(this, 'disposeInternal');
};
/**
* The message event.
*
* @param {!Array} array The data array from the underlying channel.
* @constructor
* @extends {goog.net.WebChannel.MessageEvent}
*/
WebChannelBaseTransport.Channel.MessageEvent = function(array) {
goog.base(this);
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}
*/
WebChannelBaseTransport.Channel.ErrorEvent = function(error) {
goog.base(this);
/**
* 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 the {@link WebChannelBase.Handler} interface.
*
* @param {!WebChannelBaseTransport.Channel} channel The enclosing WebChannel.
*
* @constructor
* @extends {WebChannelBase.Handler}
* @private
*/
WebChannelBaseTransport.Channel.Handler_ = function(channel) {
goog.base(this);
/**
* @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);
};
}); // goog.scope

View File

@@ -0,0 +1,298 @@
// 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 {//visibility:private}
*/
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
*/
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

View File

@@ -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.
/**
* @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.
* @return {!goog.net.WebChannel} the newly created WebChannel instance.
*/
goog.net.WebChannelTransport.prototype.createWebChannel = goog.abstractMethod;

View File

@@ -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));

View File

@@ -0,0 +1,449 @@
// 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.TimeoutError');
goog.require('goog.debug.Error');
goog.require('goog.json');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XmlHttp');
goog.require('goog.result');
goog.require('goog.result.SimpleResult');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.scope(function() {
var _ = goog.labs.net.xhr;
var Result = goog.result.Result;
var SimpleResult = goog.result.SimpleResult;
var Wait = goog.result.wait;
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://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
* - mimeType: allows the caller to override the content-type and charset for
* the request, which is useful when requesting binary data. See:
* http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
* - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
* be removed before parsing the response as JSON.
*
* @typedef {{
* headers: (Object.<string>|undefined),
* timeoutMs: (number|undefined),
* withCredentials: (boolean|undefined),
* mimeType: (string|undefined),
* xssiPrefix: (string|undefined)
* }}
*/
_.Options;
/**
* Defines the types that are allowed as post data.
* @typedef {(ArrayBuffer|Blob|Document|FormData|null|string|undefined)}
*/
_.PostData;
/**
* The Content-Type HTTP header name.
* @type {string}
*/
_.CONTENT_TYPE_HEADER = 'Content-Type';
/**
* The Content-Type HTTP header value for a url-encoded form.
* @type {string}
*/
_.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';
/**
* Sends a get request, returning a transformed result which will be resolved
* with the response text once the request completes.
*
* @param {string} url The URL to request.
* @param {_.Options=} opt_options Configuration options for the request.
* @return {!Result} A result object that will be resolved
* with the response text once the request finishes.
*/
_.get = function(url, opt_options) {
var result = _.send('GET', url, null, opt_options);
var transformedResult = goog.result.transform(result,
_.getResponseText_);
return transformedResult;
};
/**
* Sends a post request, returning a transformed result which will be resolved
* with the response text once the request completes.
*
* @param {string} url The URL to request.
* @param {_.PostData} data The body of the post request.
* @param {_.Options=} opt_options Configuration options for the request.
* @return {!Result} A result object that will be resolved
* with the response text once the request finishes.
*/
_.post = function(url, data, opt_options) {
var result = _.send('POST', url, data, opt_options);
var transformedResult = goog.result.transform(result,
_.getResponseText_);
return transformedResult;
};
/**
* Sends a get request, returning a result which will be resolved with
* the parsed response text once the request completes.
*
* @param {string} url The URL to request.
* @param {_.Options=} opt_options Configuration options for the request.
* @return {!Result} A result object that will be resolved
* with the response JSON once the request finishes.
*/
_.getJson = function(url, opt_options) {
var result = _.send('GET', url, null, opt_options);
var transformedResult = _.addJsonParsingCallbacks_(result, opt_options);
return transformedResult;
};
/**
* Sends a post request, returning a result which will be resolved with
* the parsed response text once the request completes.
*
* @param {string} url The URL to request.
* @param {_.PostData} data The body of the post request.
* @param {_.Options=} opt_options Configuration options for the request.
* @return {!Result} A result object that will be resolved
* with the response JSON once the request finishes.
*/
_.postJson = function(url, data, opt_options) {
var result = _.send('POST', url, data, opt_options);
var transformedResult = _.addJsonParsingCallbacks_(result, opt_options);
return transformedResult;
};
/**
* Sends a request using XMLHttpRequest and returns a result.
*
* @param {string} method The HTTP method for the request.
* @param {string} url The URL to request.
* @param {_.PostData} data The body of the post request.
* @param {_.Options=} opt_options Configuration options for the request.
* @return {!Result} A result object that will be resolved
* with the XHR object as it's value when the request finishes.
*/
_.send = function(method, url, data, opt_options) {
var result = new SimpleResult();
// When the deferred is cancelled, we abort the XHR. We want to make sure
// the readystatechange event still fires, so it can do the timeout
// cleanup, however we don't want the callback or errback to be called
// again. Thus the slight ugliness here. If results were pushed into
// makeRequest, this could become a lot cleaner but we want an option for
// people not to include goog.result.Result.
goog.result.waitOnError(result, function(error, result) {
if (result.isCanceled()) {
xhr.abort();
xhr.onreadystatechange = goog.nullFunction;
}
});
function callback(data) {
result.setValue(data);
}
function errback(err) {
result.setError(err);
}
var xhr = _.makeRequest(method, url, data, opt_options, callback, errback);
return result;
};
/**
* Creates a new XMLHttpRequest and initiates a request.
*
* @param {string} method The HTTP method for the request.
* @param {string} url The URL to request.
* @param {_.PostData} data The body of the post request, unless the content
* type is explicitly set in the Options, then it will default to form
* urlencoded.
* @param {_.Options=} opt_options Configuration options for the request.
* @param {function(XMLHttpRequest)=} opt_callback Optional callback to call
* when the request completes.
* @param {function(Error)=} opt_errback Optional callback to call
* when there is an error.
* @return {!XMLHttpRequest} The new XMLHttpRequest.
*/
_.makeRequest = function(
method, url, data, opt_options, opt_callback, opt_errback) {
var options = opt_options || {};
var callback = opt_callback || goog.nullFunction;
var errback = opt_errback || goog.nullFunction;
var timer;
var xhr = /** @type {!XMLHttpRequest} */ (goog.net.XmlHttp());
try {
xhr.open(method, url, true);
} catch (e) {
// XMLHttpRequest.open may throw when 'open' is called, for example, IE7
// throws "Access Denied" for cross-origin requests.
errback(new _.Error('Error opening XHR: ' + e.message, url, xhr));
return xhr;
}
// So sad that IE doesn't support onload and onerror.
xhr.onreadystatechange = function() {
if (xhr.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
window.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(xhr.status) ||
xhr.status === 0 && !_.isEffectiveSchemeHttp_(url)) {
callback(xhr);
} else {
errback(new _.HttpError(xhr.status, url, xhr));
}
}
};
// Set the headers.
var contentTypeIsSet = false;
if (options.headers) {
for (var key in options.headers) {
xhr.setRequestHeader(key, options.headers[key]);
}
contentTypeIsSet = _.CONTENT_TYPE_HEADER in options.headers;
}
// If a content type hasn't been set, 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' && !contentTypeIsSet) {
xhr.setRequestHeader(_.CONTENT_TYPE_HEADER, _.FORM_CONTENT_TYPE);
}
// Set whether to pass cookies on cross-domain requests (if applicable).
// @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
if (options.withCredentials) {
xhr.withCredentials = options.withCredentials;
}
// Allow the request to override the mime type, useful for getting binary
// data from the server. e.g. 'text/plain; charset=x-user-defined'.
// @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
if (options.mimeType) {
xhr.overrideMimeType(options.mimeType);
}
// Handle timeouts, if requested.
if (options.timeoutMs > 0) {
timer = window.setTimeout(function() {
// Clear event listener before aborting so the errback will not be
// called twice.
xhr.onreadystatechange = goog.nullFunction;
xhr.abort();
errback(new _.TimeoutError(url, xhr));
}, options.timeoutMs);
}
// Trigger the send.
try {
xhr.send(data);
} catch (e) {
// XMLHttpRequest.send is known to throw on some versions of FF, for example
// if a cross-origin request is disallowed.
xhr.onreadystatechange = goog.nullFunction;
window.clearTimeout(timer);
errback(new _.Error('Error sending XHR: ' + e.message, url, xhr));
}
return xhr;
};
/**
* @param {string} url The URL to test.
* @return {boolean} Whether the effective scheme is HTTP or HTTPs.
* @private
*/
_.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 == '';
};
/**
* Returns the response text of an XHR object. Intended to be called when
* the result resolves.
*
* @param {!XMLHttpRequest} xhr The XHR object.
* @return {string} The response text.
* @private
*/
_.getResponseText_ = function(xhr) {
return xhr.responseText;
};
/**
* Transforms a result, parsing the JSON in the original result value's
* responseText. The transformed result's value is a javascript object.
* Parse errors resolve the transformed result in an error.
*
* @param {!Result} result The result to wait on.
* @param {_.Options|undefined} options The options object.
*
* @return {!Result} The transformed result.
* @private
*/
_.addJsonParsingCallbacks_ = function(result, options) {
var resultWithResponseText = goog.result.transform(result,
_.getResponseText_);
var prefixStrippedResult = resultWithResponseText;
if (options && options.xssiPrefix) {
prefixStrippedResult = goog.result.transform(resultWithResponseText,
goog.partial(_.stripXssiPrefix_, options.xssiPrefix));
}
var jsonParsedResult = goog.result.transform(prefixStrippedResult,
goog.json.parse);
return jsonParsedResult;
};
/**
* 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
*/
_.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 {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
* @extends {goog.debug.Error}
* @constructor
*/
_.Error = function(message, url, xhr) {
goog.base(this, message + ', url=' + url);
/**
* The URL that was requested.
* @type {string}
*/
this.url = url;
/**
* The XMLHttpRequest corresponding with the failed request.
* @type {!XMLHttpRequest}
*/
this.xhr = xhr;
};
goog.inherits(_.Error, goog.debug.Error);
/** @override */
_.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 {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
* @extends {_.Error}
* @constructor
*/
_.HttpError = function(status, url, xhr) {
goog.base(this, 'Request Failed, status=' + status, url, xhr);
/**
* The HTTP status code for the error.
* @type {number}
*/
this.status = status;
};
goog.inherits(_.HttpError, _.Error);
/** @override */
_.HttpError.prototype.name = 'XhrHttpError';
/**
* Class for Timeout errors.
*
* @param {string} url The URL that timed out.
* @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
* @extends {_.Error}
* @constructor
*/
_.TimeoutError = function(url, xhr) {
goog.base(this, 'Request timed out', url, xhr);
};
goog.inherits(_.TimeoutError, _.Error);
/** @override */
_.TimeoutError.prototype.name = 'XhrTimeoutError';
}); // goog.scope

View File

@@ -0,0 +1,46 @@
// 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.
*/
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;
};

View File

@@ -0,0 +1,63 @@
// 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 notice object that is used to encapsulates
* information about a particular change/notification on an observable
* object.
*/
goog.provide('goog.labs.observe.Notice');
/**
* A notice object encapsulates information about a notification fired
* by an observable.
* @param {!goog.labs.observe.Observable} observable The observable
* object that fires this notice.
* @param {*=} opt_data The optional data associated with this notice.
* @constructor
*/
goog.labs.observe.Notice = function(observable, opt_data) {
/**
* @type {!goog.labs.observe.Observable}
* @private
*/
this.observable_ = observable;
/**
* @type {*}
* @private
*/
this.data_ = opt_data;
};
/**
* @return {!goog.labs.observe.Observable} The observable object that
* fires this notice.
*/
goog.labs.observe.Notice.prototype.getObservable = function() {
return this.observable_;
};
/**
* @return {*} The optional data associated with this notice. May be
* null/undefined.
*/
goog.labs.observe.Notice.prototype.getData = function() {
return this.data_;
};

View File

@@ -0,0 +1,77 @@
// 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 Experimental observer-observable API. This is
* intended as super lightweight replacement of
* goog.events.EventTarget when w3c event model bubble/capture
* behavior is not required.
*
* This is similar to {@code goog.pubsub.PubSub} but with different
* intent and naming so that it is more discoverable. The API is
* tighter while allowing for more flexibility offered by the
* interface {@code Observable}.
*
* WARNING: This is still highly experimental. Please contact author
* before using this.
*
*/
goog.provide('goog.labs.observe.Observable');
goog.require('goog.disposable.IDisposable');
/**
* Interface for an observable object.
* @interface
* @extends {goog.disposable.IDisposable}
*/
goog.labs.observe.Observable = function() {};
/**
* Registers an observer on the observable.
*
* Note that no guarantee is provided on order of execution of the
* observers. For a single notification, one Notice object is reused
* across all invoked observers.
*
* Note that if an observation with the same observer is already
* registered, it will not be registered again. Comparison is done via
* observer's {@code equals} method.
*
* @param {!goog.labs.observe.Observer} observer The observer to add.
* @return {boolean} Whether the observer was successfully added.
*/
goog.labs.observe.Observable.prototype.observe = function(observer) {};
/**
* Unregisters an observer from the observable. The parameter must be
* the same as those passed to {@code observe} method. Comparison is
* done via observer's {@code equals} method.
* @param {!goog.labs.observe.Observer} observer The observer to remove.
* @return {boolean} Whether the observer is removed.
*/
goog.labs.observe.Observable.prototype.unobserve = function(observer) {};
/**
* Notifies observers by invoking them. Optionally, a data object may be
* given to be passed to each observer.
* @param {*=} opt_data An optional data object.
*/
goog.labs.observe.Observable.prototype.notify = function(opt_data) {};

View File

@@ -0,0 +1,180 @@
// 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 set of {@code goog.labs.observe.Observable}s that
* allow registering and removing observers to all of the observables
* in the set.
*/
goog.provide('goog.labs.observe.ObservableSet');
goog.require('goog.array');
goog.require('goog.labs.observe.Observer');
/**
* Creates a set of observables.
*
* An ObservableSet is a collection of observables. Observers may be
* reigstered and will receive notifications when any of the
* observables notify. This class is meant to simplify management of
* observations on multiple observables of the same nature.
*
* @constructor
*/
goog.labs.observe.ObservableSet = function() {
/**
* The observers registered with this set.
* @type {!Array.<!goog.labs.observe.Observer>}
* @private
*/
this.observers_ = [];
/**
* The observables in this set.
* @type {!Array.<!goog.labs.observe.Observable>}
* @private
*/
this.observables_ = [];
};
/**
* Adds an observer that observes all observables in the set. If new
* observables are added to or removed from the set, the observer will
* be registered or unregistered accordingly.
*
* The observer will not be added if there is already an equivalent
* observer.
*
* @param {!goog.labs.observe.Observer} observer The observer to invoke.
* @return {boolean} Whether the observer is actually added.
*/
goog.labs.observe.ObservableSet.prototype.addObserver = function(observer) {
// Check whether the observer already exists.
if (goog.array.find(this.observers_, goog.partial(
goog.labs.observe.Observer.equals, observer))) {
return false;
}
this.observers_.push(observer);
goog.array.forEach(this.observables_, function(o) {
o.observe(observer);
});
return true;
};
/**
* Removes an observer from the set. The observer will be removed from
* all observables in the set. Does nothing if the observer is not in
* the set.
* @param {!goog.labs.observe.Observer} observer The observer to remove.
* @return {boolean} Whether the observer is actually removed.
*/
goog.labs.observe.ObservableSet.prototype.removeObserver = function(observer) {
// Check that the observer exists before removing.
var removed = goog.array.removeIf(this.observers_, goog.partial(
goog.labs.observe.Observer.equals, observer));
if (removed) {
goog.array.forEach(this.observables_, function(o) {
o.unobserve(observer);
});
}
return removed;
};
/**
* Removes all registered observers.
*/
goog.labs.observe.ObservableSet.prototype.removeAllObservers = function() {
this.unregisterAll_();
this.observers_.length = 0;
};
/**
* Adds an observable to the set. All previously added and future
* observers will be added to the new observable as well.
*
* The observable will not be added if it is already registered in the
* set.
*
* @param {!goog.labs.observe.Observable} observable The observable to add.
* @return {boolean} Whether the observable is actually added.
*/
goog.labs.observe.ObservableSet.prototype.addObservable = function(observable) {
if (goog.array.contains(this.observables_, observable)) {
return false;
}
this.observables_.push(observable);
goog.array.forEach(this.observers_, function(observer) {
observable.observe(observer);
});
return true;
};
/**
* Removes an observable from the set. All observers registered on the
* set will be removed from the observable as well.
* @param {!goog.labs.observe.Observable} observable The observable to remove.
* @return {boolean} Whether the observable is actually removed.
*/
goog.labs.observe.ObservableSet.prototype.removeObservable = function(
observable) {
var removed = goog.array.remove(this.observables_, observable);
if (removed) {
goog.array.forEach(this.observers_, function(observer) {
observable.unobserve(observer);
});
}
return removed;
};
/**
* Removes all registered observables.
*/
goog.labs.observe.ObservableSet.prototype.removeAllObservables = function() {
this.unregisterAll_();
this.observables_.length = 0;
};
/**
* Removes all registered observations and observables.
*/
goog.labs.observe.ObservableSet.prototype.removeAll = function() {
this.removeAllObservers();
this.observables_.length = 0;
};
/**
* Unregisters all registered observers from all registered observables.
* @private
*/
goog.labs.observe.ObservableSet.prototype.unregisterAll_ = function() {
goog.array.forEach(this.observers_, function(observer) {
goog.array.forEach(this.observables_, function(o) {
o.unobserve(observer);
});
}, this);
};

View File

@@ -0,0 +1,156 @@
// 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 set of observations. This set provides a convenient
* means of observing many observables at once.
*
* This is similar in purpose to {@code goog.events.EventHandler}.
*
*/
goog.provide('goog.labs.observe.ObservationSet');
goog.require('goog.array');
goog.require('goog.labs.observe.Observer');
/**
* A set of observations. An observation is defined by an observable
* and an observer. The set keeps track of observations and
* allows their removal.
* @param {!Object=} opt_defaultScope Optional function scope to use
* when using {@code observeWithFunction} and
* {@code unobserveWithFunction}.
* @constructor
*/
goog.labs.observe.ObservationSet = function(opt_defaultScope) {
/**
* @type {!Array.<!goog.labs.observe.ObservationSet.Observation_>}
* @private
*/
this.storedObservations_ = [];
/**
* @type {!Object|undefined}
* @private
*/
this.defaultScope_ = opt_defaultScope;
};
/**
* Observes the given observer on the observable.
* @param {!goog.labs.observe.Observable} observable The observable to
* observe on.
* @param {!goog.labs.observe.Observer} observer The observer.
* @return {boolean} True if the observer is successfully registered.
*/
goog.labs.observe.ObservationSet.prototype.observe = function(
observable, observer) {
var success = observable.observe(observer);
if (success) {
this.storedObservations_.push(
new goog.labs.observe.ObservationSet.Observation_(
observable, observer));
}
return success;
};
/**
* Observes the given function on the observable.
* @param {!goog.labs.observe.Observable} observable The observable to
* observe on.
* @param {function(!goog.labs.observe.Notice)} fn The handler function.
* @param {!Object=} opt_scope Optional scope.
* @return {goog.labs.observe.Observer} The registered observer object.
* If the observer is not successfully registered, this will be null.
*/
goog.labs.observe.ObservationSet.prototype.observeWithFunction = function(
observable, fn, opt_scope) {
var observer = goog.labs.observe.Observer.fromFunction(
fn, opt_scope || this.defaultScope_);
if (this.observe(observable, observer)) {
return observer;
}
return null;
};
/**
* Unobserves the given observer from the observable.
* @param {!goog.labs.observe.Observable} observable The observable to
* unobserve from.
* @param {!goog.labs.observe.Observer} observer The observer.
* @return {boolean} True if the observer is successfully removed.
*/
goog.labs.observe.ObservationSet.prototype.unobserve = function(
observable, observer) {
var removed = goog.array.removeIf(
this.storedObservations_, function(o) {
return o.observable == observable &&
goog.labs.observe.Observer.equals(o.observer, observer);
});
if (removed) {
observable.unobserve(observer);
}
return removed;
};
/**
* Unobserves the given function from the observable.
* @param {!goog.labs.observe.Observable} observable The observable to
* unobserve from.
* @param {function(!goog.labs.observe.Notice)} fn The handler function.
* @param {!Object=} opt_scope Optional scope.
* @return {boolean} True if the observer is successfully removed.
*/
goog.labs.observe.ObservationSet.prototype.unobserveWithFunction = function(
observable, fn, opt_scope) {
var observer = goog.labs.observe.Observer.fromFunction(
fn, opt_scope || this.defaultScope_);
return this.unobserve(observable, observer);
};
/**
* Removes all observations registered through this set.
*/
goog.labs.observe.ObservationSet.prototype.removeAll = function() {
goog.array.forEach(this.storedObservations_, function(observation) {
var observable = observation.observable;
var observer = observation.observer;
observable.unobserve(observer);
});
};
/**
* A representation of an observation, which is defined uniquely by
* the observable and observer.
* @param {!goog.labs.observe.Observable} observable The observable.
* @param {!goog.labs.observe.Observer} observer The observer.
* @constructor
* @private
*/
goog.labs.observe.ObservationSet.Observation_ = function(
observable, observer) {
this.observable = observable;
this.observer = observer;
};

View File

@@ -0,0 +1,100 @@
// 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 Provide definition of an observer. This is meant to
* be used with {@code goog.labs.observe.Observable}.
*
* This file also provides convenient functions to compare and create
* Observer objects.
*
*/
goog.provide('goog.labs.observe.Observer');
/**
* A class implementing {@code Observer} may be informed of changes in
* observable object.
* @see {goog.labs.observe.Observable}
* @interface
*/
goog.labs.observe.Observer = function() {};
/**
* Notifies the observer of changes to the observable object.
* @param {!goog.labs.observe.Notice} notice The notice object.
*/
goog.labs.observe.Observer.prototype.notify;
/**
* Whether this observer is equal to the given observer.
* @param {!goog.labs.observe.Observer} observer The observer to compare with.
* @return {boolean} Whether the two observers are equal.
*/
goog.labs.observe.Observer.prototype.equals;
/**
* @param {!goog.labs.observe.Observer} observer1 Observer to compare.
* @param {!goog.labs.observe.Observer} observer2 Observer to compare.
* @return {boolean} Whether observer1 and observer2 are equal, as
* determined by the first observer1's {@code equals} method.
*/
goog.labs.observe.Observer.equals = function(observer1, observer2) {
return observer1 == observer2 || observer1.equals(observer2);
};
/**
* Creates an observer that calls the given function.
* @param {function(!goog.labs.observe.Notice)} fn Function to be converted.
* @param {!Object=} opt_scope Optional scope to execute the function.
* @return {!goog.labs.observe.Observer} An observer object.
*/
goog.labs.observe.Observer.fromFunction = function(fn, opt_scope) {
return new goog.labs.observe.Observer.FunctionObserver_(fn, opt_scope);
};
/**
* An observer that calls the given function on {@code notify}.
* @param {function(!goog.labs.observe.Notice)} fn Function to delegate to.
* @param {!Object=} opt_scope Optional scope to execute the function.
* @constructor
* @implements {goog.labs.observe.Observer}
* @private
*/
goog.labs.observe.Observer.FunctionObserver_ = function(fn, opt_scope) {
this.fn_ = fn;
this.scope_ = opt_scope;
};
/** @override */
goog.labs.observe.Observer.FunctionObserver_.prototype.notify = function(
notice) {
this.fn_.call(this.scope_, notice);
};
/** @override */
goog.labs.observe.Observer.FunctionObserver_.prototype.equals = function(
observer) {
return this.fn_ === observer.fn_ && this.scope_ === observer.scope_;
};

View File

@@ -0,0 +1,129 @@
// 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 An implementation of {@code Observable} that can be
* used as base class or composed into another class that wants to
* implement {@code Observable}.
*/
goog.provide('goog.labs.observe.SimpleObservable');
goog.require('goog.Disposable');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.labs.observe.Notice');
goog.require('goog.labs.observe.Observable');
goog.require('goog.labs.observe.Observer');
goog.require('goog.object');
/**
* A simple implementation of {@code goog.labs.observe.Observable} that can
* be used as a standalone observable or as a base class for other
* observable object.
*
* When another class wants to implement observable without extending
* {@code SimpleObservable}, they can create an instance of
* {@code SimpleObservable}, specifying {@code opt_actualObservable},
* and delegate to the instance. Here is a trivial example:
*
* <pre>
* ClassA = function() {
* goog.base(this);
* this.observable_ = new SimpleObservable(this);
* this.registerDisposable(this.observable_);
* };
* goog.inherits(ClassA, goog.Disposable);
*
* ClassA.prototype.observe = function(observer) {
* this.observable_.observe(observer);
* };
*
* ClassA.prototype.unobserve = function(observer) {
* this.observable_.unobserve(observer);
* };
*
* ClassA.prototype.notify = function(opt_data) {
* this.observable_.notify(opt_data);
* };
* </pre>
*
* @param {!goog.labs.observe.Observable=} opt_actualObservable
* Optional observable object. Defaults to 'this'. When used as
* base class, the parameter need not be given. It is only useful
* when using this class to implement implement {@code Observable}
* interface on another object, see example above.
* @constructor
* @implements {goog.labs.observe.Observable}
* @extends {goog.Disposable}
*/
goog.labs.observe.SimpleObservable = function(opt_actualObservable) {
goog.base(this);
/**
* @type {!goog.labs.observe.Observable}
* @private
*/
this.actualObservable_ = opt_actualObservable || this;
/**
* Observers registered on this object.
* @type {!Array.<!goog.labs.observe.Observer>}
* @private
*/
this.observers_ = [];
};
goog.inherits(goog.labs.observe.SimpleObservable, goog.Disposable);
/** @override */
goog.labs.observe.SimpleObservable.prototype.observe = function(observer) {
goog.asserts.assert(!this.isDisposed());
// Registers the (type, observer) only if it has not been previously
// registered.
var shouldRegisterObserver = !goog.array.some(this.observers_, goog.partial(
goog.labs.observe.Observer.equals, observer));
if (shouldRegisterObserver) {
this.observers_.push(observer);
}
return shouldRegisterObserver;
};
/** @override */
goog.labs.observe.SimpleObservable.prototype.unobserve = function(observer) {
goog.asserts.assert(!this.isDisposed());
return goog.array.removeIf(this.observers_, goog.partial(
goog.labs.observe.Observer.equals, observer));
};
/** @override */
goog.labs.observe.SimpleObservable.prototype.notify = function(opt_data) {
goog.asserts.assert(!this.isDisposed());
var notice = new goog.labs.observe.Notice(this.actualObservable_, opt_data);
goog.array.forEach(
goog.array.clone(this.observers_), function(observer) {
observer.notify(notice);
});
};
/** @override */
goog.labs.observe.SimpleObservable.prototype.disposeInternal = function() {
this.observers_.length = 0;
};

View File

@@ -0,0 +1,339 @@
// 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).
*/
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
*/
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_;
/**
* 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(user): 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
};

View File

@@ -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.
/**
* @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.
*/
goog.provide('goog.labs.structs.mapPerf');
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}
*/
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

View File

@@ -0,0 +1,279 @@
// 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.
*
*/
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
*/
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.<string>} */ (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.<string>} */ (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 {string} 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.<string>} */ (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;
};

View File

@@ -0,0 +1,178 @@
// 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}
*/
goog.labs.style.PixelDensityMonitor = function(opt_domHelper) {
goog.base(this);
/**
* @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.base(this, 'disposeInternal');
};

View File

@@ -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());
}

View File

@@ -0,0 +1,59 @@
// 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.asserts');
goog.require('goog.debug.Error');
goog.require('goog.labs.testing.Matcher');
/**
* 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}
*/
goog.labs.testing.MatcherError = function(opt_message) {
goog.base(this, opt_message);
};
goog.inherits(goog.labs.testing.MatcherError, goog.debug.Error);

View File

@@ -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 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}
*/
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;
}

View File

@@ -0,0 +1,266 @@
// 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.array');
goog.require('goog.asserts');
goog.require('goog.labs.testing.Matcher');
goog.require('goog.string');
/**
* The HasEntries matcher.
*
* @param {!Object} entries The entries to check in the object.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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);
}

View File

@@ -0,0 +1,206 @@
// 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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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);
}

View File

@@ -0,0 +1,51 @@
// 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) {};

View File

@@ -0,0 +1,334 @@
// 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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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
* @implements {goog.labs.testing.Matcher}
*/
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);
}

View File

@@ -0,0 +1,306 @@
// 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 object matchers like equalsObject,
* hasProperty, instanceOf, etc.
*/
goog.provide('goog.labs.testing.HasPropertyMatcher');
goog.provide('goog.labs.testing.InstanceOfMatcher');
goog.provide('goog.labs.testing.IsNullMatcher');
goog.provide('goog.labs.testing.IsNullOrUndefinedMatcher');
goog.provide('goog.labs.testing.IsUndefinedMatcher');
goog.provide('goog.labs.testing.ObjectEqualsMatcher');
goog.require('goog.labs.testing.Matcher');
goog.require('goog.string');
/**
* The Equals matcher.
*
* @param {!Object} expectedObject The expected object.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.ObjectEqualsMatcher = function(expectedObject) {
/**
* @type {!Object}
* @private
*/
this.object_ = expectedObject;
};
/**
* Determines if two objects are the same.
*
* @override
*/
goog.labs.testing.ObjectEqualsMatcher.prototype.matches =
function(actualObject) {
return actualObject === this.object_;
};
/**
* @override
*/
goog.labs.testing.ObjectEqualsMatcher.prototype.describe =
function(actualObject) {
return 'Input object is not the same as the expected object.';
};
/**
* The HasProperty matcher.
*
* @param {string} property Name of the property to test.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.HasPropertyMatcher = function(property) {
/**
* @type {string}
* @private
*/
this.property_ = property;
};
/**
* Determines if an object has a property.
*
* @override
*/
goog.labs.testing.HasPropertyMatcher.prototype.matches =
function(actualObject) {
return this.property_ in actualObject;
};
/**
* @override
*/
goog.labs.testing.HasPropertyMatcher.prototype.describe =
function(actualObject) {
return 'Object does not have property: ' + this.property_;
};
/**
* The InstanceOf matcher.
*
* @param {!Object} object The expected class object.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.InstanceOfMatcher = function(object) {
/**
* @type {!Object}
* @private
*/
this.object_ = object;
};
/**
* Determines if an object is an instance of another object.
*
* @override
*/
goog.labs.testing.InstanceOfMatcher.prototype.matches =
function(actualObject) {
return actualObject instanceof this.object_;
};
/**
* @override
*/
goog.labs.testing.InstanceOfMatcher.prototype.describe =
function(actualObject) {
return 'Input object is not an instance of the expected object';
};
/**
* The IsNullOrUndefined matcher.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.IsNullOrUndefinedMatcher = function() {};
/**
* Determines if input value is null or undefined.
*
* @override
*/
goog.labs.testing.IsNullOrUndefinedMatcher.prototype.matches =
function(actualValue) {
return !goog.isDefAndNotNull(actualValue);
};
/**
* @override
*/
goog.labs.testing.IsNullOrUndefinedMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' is not null or undefined.';
};
/**
* The IsNull matcher.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.IsNullMatcher = function() {};
/**
* Determines if input value is null.
*
* @override
*/
goog.labs.testing.IsNullMatcher.prototype.matches =
function(actualValue) {
return goog.isNull(actualValue);
};
/**
* @override
*/
goog.labs.testing.IsNullMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' is not null.';
};
/**
* The IsUndefined matcher.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.IsUndefinedMatcher = function() {};
/**
* Determines if input value is undefined.
*
* @override
*/
goog.labs.testing.IsUndefinedMatcher.prototype.matches =
function(actualValue) {
return !goog.isDef(actualValue);
};
/**
* @override
*/
goog.labs.testing.IsUndefinedMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' is not undefined.';
};
/**
* Returns a matcher that matches objects that are equal to the input object.
* Equality in this case means the two objects are references to the same
* object.
*
* @param {!Object} object The expected object.
*
* @return {!goog.labs.testing.ObjectEqualsMatcher} A
* ObjectEqualsMatcher.
*/
function equalsObject(object) {
return new goog.labs.testing.ObjectEqualsMatcher(object);
}
/**
* Returns a matcher that matches objects that contain the input property.
*
* @param {string} property The property name to check.
*
* @return {!goog.labs.testing.HasPropertyMatcher} A HasPropertyMatcher.
*/
function hasProperty(property) {
return new goog.labs.testing.HasPropertyMatcher(property);
}
/**
* Returns a matcher that matches instances of the input class.
*
* @param {!Object} object The class object.
*
* @return {!goog.labs.testing.InstanceOfMatcher} A
* InstanceOfMatcher.
*/
function instanceOfClass(object) {
return new goog.labs.testing.InstanceOfMatcher(object);
}
/**
* Returns a matcher that matches all null values.
*
* @return {!goog.labs.testing.IsNullMatcher} A IsNullMatcher.
*/
function isNull() {
return new goog.labs.testing.IsNullMatcher();
}
/**
* Returns a matcher that matches all null and undefined values.
*
* @return {!goog.labs.testing.IsNullOrUndefinedMatcher} A
* IsNullOrUndefinedMatcher.
*/
function isNullOrUndefined() {
return new goog.labs.testing.IsNullOrUndefinedMatcher();
}
/**
* Returns a matcher that matches undefined values.
*
* @return {!goog.labs.testing.IsUndefinedMatcher} A IsUndefinedMatcher.
*/
function isUndefined() {
return new goog.labs.testing.IsUndefinedMatcher();
}

View File

@@ -0,0 +1,402 @@
// 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 string matchers like containsString,
* startsWith, endsWith, etc.
*/
goog.provide('goog.labs.testing.ContainsStringMatcher');
goog.provide('goog.labs.testing.EndsWithMatcher');
goog.provide('goog.labs.testing.EqualToIgnoringCaseMatcher');
goog.provide('goog.labs.testing.EqualToIgnoringWhitespaceMatcher');
goog.provide('goog.labs.testing.EqualsMatcher');
goog.provide('goog.labs.testing.RegexMatcher');
goog.provide('goog.labs.testing.StartsWithMatcher');
goog.provide('goog.labs.testing.StringContainsInOrderMatcher');
goog.require('goog.asserts');
goog.require('goog.labs.testing.Matcher');
goog.require('goog.string');
/**
* The ContainsString matcher.
*
* @param {string} value The expected string.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.ContainsStringMatcher = function(value) {
/**
* @type {string}
* @private
*/
this.value_ = value;
};
/**
* Determines if input string contains the expected string.
*
* @override
*/
goog.labs.testing.ContainsStringMatcher.prototype.matches =
function(actualValue) {
goog.asserts.assertString(actualValue);
return goog.string.contains(actualValue, this.value_);
};
/**
* @override
*/
goog.labs.testing.ContainsStringMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' does not contain ' + this.value_;
};
/**
* The EndsWith matcher.
*
* @param {string} value The expected string.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.EndsWithMatcher = function(value) {
/**
* @type {string}
* @private
*/
this.value_ = value;
};
/**
* Determines if input string ends with the expected string.
*
* @override
*/
goog.labs.testing.EndsWithMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertString(actualValue);
return goog.string.endsWith(actualValue, this.value_);
};
/**
* @override
*/
goog.labs.testing.EndsWithMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' does not end with ' + this.value_;
};
/**
* The EqualToIgnoringWhitespace matcher.
*
* @param {string} value The expected string.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.EqualToIgnoringWhitespaceMatcher = function(value) {
/**
* @type {string}
* @private
*/
this.value_ = value;
};
/**
* Determines if input string contains the expected string.
*
* @override
*/
goog.labs.testing.EqualToIgnoringWhitespaceMatcher.prototype.matches =
function(actualValue) {
goog.asserts.assertString(actualValue);
var string1 = goog.string.collapseWhitespace(actualValue);
return goog.string.caseInsensitiveCompare(this.value_, string1) === 0;
};
/**
* @override
*/
goog.labs.testing.EqualToIgnoringWhitespaceMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' is not equal(ignoring whitespace) to ' + this.value_;
};
/**
* The Equals matcher.
*
* @param {string} value The expected string.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.EqualsMatcher = function(value) {
/**
* @type {string}
* @private
*/
this.value_ = value;
};
/**
* Determines if input string is equal to the expected string.
*
* @override
*/
goog.labs.testing.EqualsMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertString(actualValue);
return this.value_ === actualValue;
};
/**
* @override
*/
goog.labs.testing.EqualsMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' is not equal to ' + this.value_;
};
/**
* The MatchesRegex matcher.
*
* @param {!RegExp} regex The expected regex.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.RegexMatcher = function(regex) {
/**
* @type {!RegExp}
* @private
*/
this.regex_ = regex;
};
/**
* Determines if input string is equal to the expected string.
*
* @override
*/
goog.labs.testing.RegexMatcher.prototype.matches = function(
actualValue) {
goog.asserts.assertString(actualValue);
return this.regex_.test(actualValue);
};
/**
* @override
*/
goog.labs.testing.RegexMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' does not match ' + this.regex_;
};
/**
* The StartsWith matcher.
*
* @param {string} value The expected string.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.StartsWithMatcher = function(value) {
/**
* @type {string}
* @private
*/
this.value_ = value;
};
/**
* Determines if input string starts with the expected string.
*
* @override
*/
goog.labs.testing.StartsWithMatcher.prototype.matches = function(actualValue) {
goog.asserts.assertString(actualValue);
return goog.string.startsWith(actualValue, this.value_);
};
/**
* @override
*/
goog.labs.testing.StartsWithMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' does not start with ' + this.value_;
};
/**
* The StringContainsInOrdermatcher.
*
* @param {Array.<string>} values The expected string values.
*
* @constructor
* @implements {goog.labs.testing.Matcher}
*/
goog.labs.testing.StringContainsInOrderMatcher = function(values) {
/**
* @type {Array.<string>}
* @private
*/
this.values_ = values;
};
/**
* Determines if input string contains, in order, the expected array of strings.
*
* @override
*/
goog.labs.testing.StringContainsInOrderMatcher.prototype.matches =
function(actualValue) {
goog.asserts.assertString(actualValue);
var currentIndex, previousIndex = 0;
for (var i = 0; i < this.values_.length; i++) {
currentIndex = goog.string.contains(actualValue, this.values_[i]);
if (currentIndex < 0 || currentIndex < previousIndex) {
return false;
}
previousIndex = currentIndex;
}
return true;
};
/**
* @override
*/
goog.labs.testing.StringContainsInOrderMatcher.prototype.describe =
function(actualValue) {
return actualValue + ' does not contain the expected values in order.';
};
/**
* Matches a string containing the given string.
*
* @param {string} value The expected value.
*
* @return {!goog.labs.testing.ContainsStringMatcher} A
* ContainsStringMatcher.
*/
function containsString(value) {
return new goog.labs.testing.ContainsStringMatcher(value);
}
/**
* Matches a string that ends with the given string.
*
* @param {string} value The expected value.
*
* @return {!goog.labs.testing.EndsWithMatcher} A
* EndsWithMatcher.
*/
function endsWith(value) {
return new goog.labs.testing.EndsWithMatcher(value);
}
/**
* Matches a string that equals (ignoring whitespace) the given string.
*
* @param {string} value The expected value.
*
* @return {!goog.labs.testing.EqualToIgnoringWhitespaceMatcher} A
* EqualToIgnoringWhitespaceMatcher.
*/
function equalToIgnoringWhitespace(value) {
return new goog.labs.testing.EqualToIgnoringWhitespaceMatcher(value);
}
/**
* Matches a string that equals the given string.
*
* @param {string} value The expected value.
*
* @return {!goog.labs.testing.EqualsMatcher} A EqualsMatcher.
*/
function equals(value) {
return new goog.labs.testing.EqualsMatcher(value);
}
/**
* Matches a string against a regular expression.
*
* @param {!RegExp} regex The expected regex.
*
* @return {!goog.labs.testing.RegexMatcher} A RegexMatcher.
*/
function matchesRegex(regex) {
return new goog.labs.testing.RegexMatcher(regex);
}
/**
* Matches a string that starts with the given string.
*
* @param {string} value The expected value.
*
* @return {!goog.labs.testing.StartsWithMatcher} A
* StartsWithMatcher.
*/
function startsWith(value) {
return new goog.labs.testing.StartsWithMatcher(value);
}
/**
* Matches a string that contains the given strings in order.
*
* @param {Array.<string>} values The expected value.
*
* @return {!goog.labs.testing.StringContainsInOrderMatcher} A
* StringContainsInOrderMatcher.
*/
function stringContainsInOrder(values) {
return new goog.labs.testing.StringContainsInOrderMatcher(values);
}

View File

@@ -0,0 +1,189 @@
// 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 Closure user agent detection (Browser).
* @see <a href="http://www.useragentstring.com/">User agent strings</a>
* For more information on rendering engine, platform, or device see the other
* sub-namespaces in goog.labs.userAgent, goog.labs.userAgent.platform,
* goog.labs.userAgent.device respectively.)
*
*/
goog.provide('goog.labs.userAgent.browser');
goog.require('goog.asserts');
goog.require('goog.labs.userAgent.util');
goog.require('goog.memoize');
goog.require('goog.string');
/**
* @return {boolean} Whether the user's browser is Opera.
* @private
*/
goog.labs.userAgent.browser.matchOpera_ = goog.memoize(
goog.partial(goog.labs.userAgent.util.matchUserAgent, 'Opera'));
/**
* @return {boolean} Whether the user's browser is IE.
* @private
*/
goog.labs.userAgent.browser.matchIE_ = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('Trident') ||
goog.labs.userAgent.util.matchUserAgent('MSIE');
});
/**
* @return {boolean} Whether the user's browser is Firefox.
* @private
*/
goog.labs.userAgent.browser.matchFirefox_ = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('Firefox');
});
/**
* @return {boolean} Whether the user's browser is Safari.
* @private
*/
goog.labs.userAgent.browser.matchSafari_ = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('Safari') &&
!goog.labs.userAgent.util.matchUserAgent('Chrome') &&
!goog.labs.userAgent.util.matchUserAgent('CriOS') &&
!goog.labs.userAgent.util.matchUserAgent('Android');
});
/**
* @return {boolean} Whether the user's browser is Chrome.
* @private
*/
goog.labs.userAgent.browser.matchChrome_ = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('Chrome') ||
goog.labs.userAgent.util.matchUserAgent('CriOS');
});
/**
* @return {boolean} Whether the user's browser is the Android browser.
* @private
*/
goog.labs.userAgent.browser.matchAndroidBrowser_ = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('Android') &&
!goog.labs.userAgent.util.matchUserAgent('Chrome') &&
!goog.labs.userAgent.util.matchUserAgent('CriOS');
});
/**
* @return {boolean} Whether the user's browser is Opera.
*/
goog.labs.userAgent.browser.isOpera =
goog.memoize(goog.labs.userAgent.browser.matchOpera_);
/**
* @return {boolean} Whether the user's browser is IE.
*/
goog.labs.userAgent.browser.isIE =
goog.memoize(goog.labs.userAgent.browser.matchIE_);
/**
* @return {boolean} Whether the user's browser is Firefox.
*/
goog.labs.userAgent.browser.isFirefox =
goog.memoize(goog.labs.userAgent.browser.matchFirefox_);
/**
* @return {boolean} Whether the user's browser is Safari.
*/
goog.labs.userAgent.browser.isSafari =
goog.memoize(goog.labs.userAgent.browser.matchSafari_);
/**
* @return {boolean} Whether the user's browser is Chrome.
*/
goog.labs.userAgent.browser.isChrome =
goog.memoize(goog.labs.userAgent.browser.matchChrome_);
/**
* @return {boolean} Whether the user's browser is the Android browser.
*/
goog.labs.userAgent.browser.isAndroidBrowser =
goog.memoize(goog.labs.userAgent.browser.matchAndroidBrowser_);
/**
* @return {string} The browser version or empty string if version cannot be
* determined.
*/
goog.labs.userAgent.browser.getVersion = goog.memoize(function() {
var userAgentString = goog.labs.userAgent.util.getUserAgentString();
// Special case IE since IE's version is inside the parenthesis and without
// the '/'.
if (goog.labs.userAgent.browser.isIE()) {
return goog.labs.userAgent.browser.getIEVersion_();
}
var versionTuples =
goog.labs.userAgent.util.extractVersionTuples(userAgentString);
// tuples[2] (The first X/Y tuple after the parenthesis) contains the browser
// version number.
// TODO (vbhasin): Make this check more robust.
goog.asserts.assert(versionTuples.length > 2,
'Couldn\'t extract version tuple from user agent string');
return goog.isDef(versionTuples[2][1]) ? versionTuples[2][1] : '';
});
/**
* @param {string|number} version The version to check.
* @return {boolean} Whether the browser version is higher or the same as the
* given version.
*/
goog.labs.userAgent.browser.isVersionOrHigher = function(version) {
return goog.string.compareVersions(goog.labs.userAgent.browser.getVersion(),
version) >= 0;
};
/**
* Determines IE version. More information:
* http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx
*
* @return {string}
* @private
*/
goog.labs.userAgent.browser.getIEVersion_ = goog.memoize(function() {
var gDoc = goog.global['document'];
var version;
var userAgentString = goog.labs.userAgent.util.getUserAgentString();
if (gDoc && gDoc.documentMode) {
version = gDoc.documentMode;
} else if (gDoc && gDoc.compatMode && gDoc.compatMode == 'CSS1Compat') {
version = 7;
} else {
var arr = /\b(?:MSIE|rv)\s+([^\);]+)(?:\)|;)/.exec(userAgentString);
version = arr && arr[1] ? arr[1] : '';
}
return version;
});

View File

@@ -0,0 +1,193 @@
// 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.userAgent.browser.
*/
goog.provide('goog.labs.userAgent.browserTest');
goog.require('goog.labs.userAgent.browser');
goog.require('goog.labs.userAgent.testAgents');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.userAgent.browserTest');
var propertyReplacer = new goog.testing.PropertyReplacer();
function setUp() {
// disable memoization
propertyReplacer.set(goog.memoize, 'ENABLE_MEMOIZE', false);
}
function tearDown() {
propertyReplacer.reset();
}
function testOpera() {
setGlobalUAString(goog.labs.userAgent.testAgents.OPERA_10);
assertTrue(goog.labs.userAgent.browser.isOpera());
assertVersion('10.00');
assertVersionBetween('10.00', '10.10');
setGlobalUAString(goog.labs.userAgent.testAgents.OPERA_MAC);
assertTrue(goog.labs.userAgent.browser.isOpera());
assertVersion('11.52');
assertVersionBetween('11.50', '12.00');
setGlobalUAString(goog.labs.userAgent.testAgents.OPERA_LINUX);
assertTrue(goog.labs.userAgent.browser.isOpera());
assertVersion('11.50');
assertVersionBetween('11.00', '12.00');
}
function testIE() {
setGlobalUAString(goog.labs.userAgent.testAgents.IE_6);
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('6.0');
assertVersionBetween('5.0', '7.0');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_10);
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('10.6');
assertVersionBetween('10.0', '11.0');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_9);
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('9.0');
assertVersionBetween('8.0', '10.0');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_8);
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('8.0');
assertVersionBetween('7.0', '9.0');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_8_COMPATIBILITY);
// Test Document mode override
setDocumentMode('9');
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('9');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_9_COMPATIBILITY);
setDocumentMode('9');
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('9');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_9_COMPATIBILITY);
setDocumentMode('8');
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('8');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_11);
assertTrue(goog.labs.userAgent.browser.isIE());
assertVersion('11.0');
assertVersionBetween('10.0', '12.0');
}
function testFirefox() {
setGlobalUAString(goog.labs.userAgent.testAgents.FIREFOX_19);
assertTrue(goog.labs.userAgent.browser.isFirefox());
assertVersion('19.0');
assertVersionBetween('18.0', '20.0');
setGlobalUAString(goog.labs.userAgent.testAgents.FIREFOX_WINDOWS);
assertTrue(goog.labs.userAgent.browser.isFirefox());
assertVersion('14.0.1');
assertVersionBetween('14.0', '15.0');
setGlobalUAString(goog.labs.userAgent.testAgents.FIREFOX_LINUX);
assertTrue(goog.labs.userAgent.browser.isFirefox());
assertVersion('15.0.1');
}
function testChrome() {
setGlobalUAString(goog.labs.userAgent.testAgents.CHROME_ANDROID);
assertTrue(goog.labs.userAgent.browser.isChrome());
assertVersion('18.0.1025.133');
assertVersionBetween('18.0', '19.0');
assertVersionBetween('17.0', '18.1');
setGlobalUAString(goog.labs.userAgent.testAgents.CHROME_IPHONE);
assertTrue(goog.labs.userAgent.browser.isChrome());
assertVersion('22.0.1194.0');
assertVersionBetween('22.0', '23.0');
assertVersionBetween('22.0', '22.10');
setGlobalUAString(goog.labs.userAgent.testAgents.CHROME_MAC);
assertTrue(goog.labs.userAgent.browser.isChrome());
assertVersion('24.0.1309.0');
assertVersionBetween('24.0', '25.0');
assertVersionBetween('24.0', '24.10');
}
function testSafari() {
setGlobalUAString(goog.labs.userAgent.testAgents.IPAD_6);
assertTrue(goog.labs.userAgent.browser.isSafari());
assertVersion('6.0');
assertVersionBetween('5.1', '7.0');
setGlobalUAString(goog.labs.userAgent.testAgents.SAFARI_6);
assertTrue(goog.labs.userAgent.browser.isSafari());
assertVersion('6.0');
assertVersionBetween('6.0', '7.0');
setGlobalUAString(goog.labs.userAgent.testAgents.SAFARI_IPHONE);
assertTrue(goog.labs.userAgent.browser.isSafari());
assertVersion('5.0.2');
assertVersionBetween('5.0', '6.0');
}
function testAndroidBrowser() {
setGlobalUAString(goog.labs.userAgent.testAgents.ANDROID_BROWSER_235);
assertTrue(goog.labs.userAgent.browser.isAndroidBrowser());
assertVersion('4.0');
assertVersionBetween('3.0', '5.0');
setGlobalUAString(goog.labs.userAgent.testAgents.ANDROID_BROWSER_403);
assertTrue(goog.labs.userAgent.browser.isAndroidBrowser());
assertVersion('4.0');
assertVersionBetween('3.0', '5.0');
setGlobalUAString(goog.labs.userAgent.testAgents.ANDROID_BROWSER_233);
assertTrue(goog.labs.userAgent.browser.isAndroidBrowser());
assertVersion('4.0');
assertVersionBetween('3.0', '5.0');
}
function setGlobalUAString(uaString) {
var mockGlobal = {
'navigator': {
'userAgent': uaString
}
};
propertyReplacer.set(goog, 'global', mockGlobal);
}
function setDocumentMode(docMode) {
var mockDocument = {
'documentMode': docMode
};
propertyReplacer.set(goog.global, 'document', mockDocument);
}
function assertVersion(version) {
assertEquals(version, goog.labs.userAgent.browser.getVersion());
}
function assertVersionBetween(lowVersion, highVersion) {
assertTrue(goog.labs.userAgent.browser.isVersionOrHigher(lowVersion));
assertFalse(goog.labs.userAgent.browser.isVersionOrHigher(highVersion));
}

View File

@@ -0,0 +1,64 @@
// 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 Closure user device detection (based on user agent).
* @see http://en.wikipedia.org/wiki/User_agent
* For more information on browser brand, platform, or engine see the other
* sub-namespaces in goog.labs.userAgent (browser, platform, and engine).
*
*/
goog.provide('goog.labs.userAgent.device');
goog.require('goog.labs.userAgent.util');
/**
* Currently we detect the iPhone, iPod and Android mobiles (devices that have
* both Android and Mobile in the user agent string).
*
* @return {boolean} Whether the user is using a tablet.
*/
goog.labs.userAgent.device.isMobile = goog.memoize(function() {
return !goog.labs.userAgent.device.isTablet() &&
(goog.labs.userAgent.util.matchUserAgent('iPod') ||
goog.labs.userAgent.util.matchUserAgent('iPhone') ||
goog.labs.userAgent.util.matchUserAgent('Android'));
});
/**
* Currently we detect Kindle Fire, iPad, and Android tablets (devices that have
* Android but not Mobile in the user agent string).
*
* @return {boolean} Whether the user is using a tablet.
*/
goog.labs.userAgent.device.isTablet = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('iPad') ||
(goog.labs.userAgent.util.matchUserAgent('Android') &&
!goog.labs.userAgent.util.matchUserAgent('Mobile')) ||
goog.labs.userAgent.util.matchUserAgent('Silk');
});
/**
* Currently we detect Kindle Fire, iPad, and Android tablets.
*
* @return {boolean} Whether the user is using a tablet.
*/
goog.labs.userAgent.device.isDesktop = goog.memoize(function() {
return !goog.labs.userAgent.device.isMobile() &&
!goog.labs.userAgent.device.isTablet();
});

View File

@@ -0,0 +1,88 @@
// 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.userAgent.device.
*/
goog.provide('goog.labs.userAgent.deviceTest');
goog.require('goog.labs.userAgent.device');
goog.require('goog.labs.userAgent.testAgents');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.userAgent.deviceTest');
var propertyReplacer = new goog.testing.PropertyReplacer();
function setUp() {
// disable memoization
propertyReplacer.set(goog.memoize, 'ENABLE_MEMOIZE', false);
}
function tearDown() {
propertyReplacer.reset();
}
function setGlobalUAString(uaString) {
var mockGlobal = {
'navigator': {
'userAgent': uaString
}
};
propertyReplacer.set(goog, 'global', mockGlobal);
}
function testMobile() {
assertIsMobile(goog.labs.userAgent.testAgents.ANDROID_BROWSER_235);
assertIsMobile(goog.labs.userAgent.testAgents.CHROME_ANDROID);
assertIsMobile(goog.labs.userAgent.testAgents.SAFARI_IPHONE);
}
function testTablet() {
assertIsTablet(goog.labs.userAgent.testAgents.CHROME_ANDROID_TABLET);
assertIsTablet(goog.labs.userAgent.testAgents.KINDLE_FIRE);
assertIsTablet(goog.labs.userAgent.testAgents.IPAD_6);
}
function testDesktop() {
assertIsDesktop(goog.labs.userAgent.testAgents.CHROME_25);
assertIsDesktop(goog.labs.userAgent.testAgents.OPERA_10);
assertIsDesktop(goog.labs.userAgent.testAgents.FIREFOX_19);
assertIsDesktop(goog.labs.userAgent.testAgents.IE_9);
assertIsDesktop(goog.labs.userAgent.testAgents.IE_10);
assertIsDesktop(goog.labs.userAgent.testAgents.IE_11);
}
function assertIsMobile(uaString) {
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.device.isMobile());
assertFalse(goog.labs.userAgent.device.isTablet());
assertFalse(goog.labs.userAgent.device.isDesktop());
}
function assertIsTablet(uaString) {
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.device.isTablet());
assertFalse(goog.labs.userAgent.device.isMobile());
assertFalse(goog.labs.userAgent.device.isDesktop());
}
function assertIsDesktop(uaString) {
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.device.isDesktop());
assertFalse(goog.labs.userAgent.device.isMobile());
assertFalse(goog.labs.userAgent.device.isTablet());
}

View File

@@ -0,0 +1,144 @@
// 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 Closure user agent detection.
* @see http://en.wikipedia.org/wiki/User_agent
* For more information on browser brand, platform, or device see the other
* sub-namespaces in goog.labs.userAgent (browser, platform, and device).
*
*/
goog.provide('goog.labs.userAgent.engine');
goog.require('goog.array');
goog.require('goog.labs.userAgent.util');
goog.require('goog.memoize');
goog.require('goog.string');
/**
* Returns the user agent string.
*
* @return {?string} The user agent string.
*/
goog.labs.userAgent.engine.getUserAgentString = goog.memoize(function() {
return goog.global['navigator'] ? goog.global['navigator'].userAgent : null;
});
/**
* @param {string} str
* @return {boolean} Whether the user agent contains the given string.
* @private
*/
goog.labs.userAgent.engine.matchUserAgent_ = function(str) {
var userAgentString = goog.labs.userAgent.engine.getUserAgentString();
return Boolean(userAgentString && goog.string.contains(userAgentString, str));
};
/**
* @return {boolean} Whether the rendering engine is Presto.
*/
goog.labs.userAgent.engine.isPresto = goog.memoize(
goog.partial(goog.labs.userAgent.engine.matchUserAgent_, 'Presto'));
/**
* @return {boolean} Whether the rendering engine is Trident.
*/
goog.labs.userAgent.engine.isTrident = goog.memoize(
goog.partial(goog.labs.userAgent.engine.matchUserAgent_, 'Trident'));
/**
* @return {boolean} Whether the rendering engine is WebKit.
*/
goog.labs.userAgent.engine.isWebKit = goog.memoize(
goog.partial(goog.labs.userAgent.engine.matchUserAgent_, 'WebKit'));
/**
* @return {boolean} Whether the rendering engine is Gecko.
*/
goog.labs.userAgent.engine.isGecko = goog.memoize(
goog.partial(goog.labs.userAgent.engine.matchUserAgent_, 'Gecko'));
/**
* @return {string} The rendering engine's version or empty string if version
* can't be determined.
*/
goog.labs.userAgent.engine.getVersion = goog.memoize(function() {
var userAgentString = goog.labs.userAgent.engine.getUserAgentString();
if (userAgentString) {
var tuples = goog.labs.userAgent.util.extractVersionTuples(userAgentString);
var engineTuple = tuples[1];
if (engineTuple) {
// In Gecko, the version string is either in the browser info or the
// Firefox version. See Gecko user agent string reference:
// http://goo.gl/mULqa
if (engineTuple[0] == 'Gecko') {
return goog.labs.userAgent.engine.getVersionForKey_(tuples, 'Firefox');
}
return engineTuple[1];
}
// IE has only one version identifier, and the Trident version is
// specified in the parenthetical.
var browserTuple = tuples[0];
var info;
if (browserTuple && (info = browserTuple[2])) {
var match = /Trident\/([^\s;]+)/.exec(info);
if (match) {
return match[1];
}
}
return '';
}
});
/**
* @param {string|number} version The version to check.
* @return {boolean} Whether the rendering engine version is higher or the same
* as the given version.
*/
goog.labs.userAgent.engine.isVersionOrHigher = function(version) {
return goog.string.compareVersions(goog.labs.userAgent.engine.getVersion(),
version) >= 0;
};
/**
* @param {!Array.<string>} tuples Version tuples.
* @param {string} key The key to look for.
* @return {string} The version string of the given key, if present.
* Otherwise, the empty string.
* @private
*/
goog.labs.userAgent.engine.getVersionForKey_ = function(tuples, key) {
// TODO(nnaze): Move to util if useful elsewhere.
var pair = goog.array.find(tuples, function(pair) {
return key == pair[0];
});
return pair && pair[1] || '';
};

View File

@@ -0,0 +1,164 @@
// 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.userAgent.engine.
*/
goog.provide('goog.labs.userAgent.engineTest');
goog.require('goog.labs.userAgent.engine');
goog.require('goog.labs.userAgent.testAgents');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.userAgent.engineTest');
var propertyReplacer = new goog.testing.PropertyReplacer();
function setUp() {
// disable memoization
propertyReplacer.set(goog.memoize, 'ENABLE_MEMOIZE', false);
}
function tearDown() {
propertyReplacer.reset();
}
function setGlobalUAString(uaString) {
var mockGlobal = {
'navigator': {
'userAgent': uaString
}
};
propertyReplacer.set(goog, 'global', mockGlobal);
}
function assertVersion(version) {
assertEquals(version, goog.labs.userAgent.engine.getVersion());
}
function assertLowAndHighVersions(lowVersion, highVersion) {
assertTrue(goog.labs.userAgent.engine.isVersionOrHigher(lowVersion));
assertFalse(goog.labs.userAgent.engine.isVersionOrHigher(highVersion));
}
function testPresto() {
setGlobalUAString(
'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00');
assertTrue(goog.labs.userAgent.engine.isPresto());
assertVersion('2.9.181');
assertLowAndHighVersions('2.9', '2.10');
setGlobalUAString(
'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168' +
' Version/11.52');
assertTrue(goog.labs.userAgent.engine.isPresto());
assertVersion('2.9.168');
assertLowAndHighVersions('2.9', '2.10');
setGlobalUAString(
'Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11');
assertTrue(goog.labs.userAgent.engine.isPresto());
assertVersion('2.8.131');
assertLowAndHighVersions('2.8', '2.9');
}
function testTrident() {
setGlobalUAString(
'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; ' +
'WOW64; Trident/6.0)');
assertTrue(goog.labs.userAgent.engine.isTrident());
assertVersion('6.0');
assertLowAndHighVersions('6.0', '7.0');
setGlobalUAString(
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; ' +
'Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)');
assertTrue(goog.labs.userAgent.engine.isTrident());
assertVersion('4.0');
assertLowAndHighVersions('4.0', '5.0');
setGlobalUAString(
'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)');
assertTrue(goog.labs.userAgent.engine.isTrident());
assertVersion('5.0');
assertLowAndHighVersions('5.0', '6.0');
setGlobalUAString(goog.labs.userAgent.testAgents.IE_11);
assertTrue(goog.labs.userAgent.engine.isTrident());
assertVersion('7.0');
assertLowAndHighVersions('6.0', '8.0');
}
function testWebKit() {
setGlobalUAString(
'Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; HTC Vision Build/GRI40)' +
'AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1');
assertTrue(goog.labs.userAgent.engine.isWebKit());
assertVersion('533.1');
assertLowAndHighVersions('533.0', '534.0');
setGlobalUAString(
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) ' +
'AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.370.0 Safari/533.4');
assertTrue(goog.labs.userAgent.engine.isWebKit());
assertVersion('533.4');
assertLowAndHighVersions('533.0', '534.0');
setGlobalUAString(
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) ' +
'AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.370.0 Safari/533.4');
assertTrue(goog.labs.userAgent.engine.isWebKit());
assertVersion('533.4');
assertLowAndHighVersions('533.0', '534.0');
setGlobalUAString(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 ' +
'(KHTML, like Gecko) Version/5.1.3 Safari/534.53.10');
assertTrue(goog.labs.userAgent.engine.isWebKit());
assertVersion('534.55.3');
assertLowAndHighVersions('534.0', '535.0');
setGlobalUAString(
'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 ' +
'(KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15');
assertTrue(goog.labs.userAgent.engine.isWebKit());
assertVersion('537.15');
assertLowAndHighVersions('537.0', '538.0');
}
function testGecko() {
setGlobalUAString(
'Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2');
assertTrue(goog.labs.userAgent.engine.isGecko());
assertVersion('15.0a2');
assertLowAndHighVersions('14.0', '16.0');
// This is actually not at V15 because it is alpha 2
assertFalse(goog.labs.userAgent.engine.isVersionOrHigher('15'));
setGlobalUAString(
'Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 ' +
'Firefox/16.0.1');
assertTrue(goog.labs.userAgent.engine.isGecko());
assertVersion('16.0.1');
assertLowAndHighVersions('16.0', '17.0');
setGlobalUAString('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) ' +
'Gecko/20100101 Firefox/14.0.1');
assertTrue(goog.labs.userAgent.engine.isGecko());
assertVersion('14.0.1');
assertLowAndHighVersions('14.0', '15.0');
}

View File

@@ -0,0 +1,193 @@
// 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 Closure user agent platform detection.
* @see <a href="http://www.useragentstring.com/">User agent strings</a>
* For more information on browser brand, rendering engine, or device see the
* other sub-namespaces in goog.labs.userAgent (browser, engine, and device
* respectively).
*
*/
goog.provide('goog.labs.userAgent.platform');
goog.require('goog.labs.userAgent.util');
goog.require('goog.memoize');
goog.require('goog.string');
/**
* Returns the platform string.
*
* @return {string} The platform string.
*/
goog.labs.userAgent.platform.getPlatformString = goog.memoize(function() {
return goog.global['navigator'] && goog.global['navigator'].platform ?
goog.global['navigator'].platform : '';
});
/**
* Returns the appVersion string.
*
* @return {string} The appVersion string.
*/
goog.labs.userAgent.platform.getAppVersion = goog.memoize(function() {
return goog.global['navigator'] && goog.global['navigator'].appVersion ?
goog.global['navigator'].appVersion : '';
});
/**
* @param {string} str
* @return {boolean} Whether the platform contains the given string.
* @private
*/
goog.labs.userAgent.platform.matchPlatform_ = function(str) {
var platformString = goog.labs.userAgent.platform.getPlatformString();
return goog.string.contains(platformString, str);
};
/**
* @return {boolean} Whether the platform is Android.
*/
goog.labs.userAgent.platform.isAndroid = goog.memoize(
goog.partial(goog.labs.userAgent.util.matchUserAgent, 'Android'));
/**
* @return {boolean} Whether the platform is iPod.
*/
goog.labs.userAgent.platform.isIpod = goog.memoize(
goog.partial(goog.labs.userAgent.util.matchUserAgent, 'iPod'));
/**
* @return {boolean} Whether the platform is iPhone.
*/
goog.labs.userAgent.platform.isIphone = goog.memoize(function() {
return goog.labs.userAgent.util.matchUserAgent('iPhone') &&
!goog.labs.userAgent.util.matchUserAgent('iPod');
});
/**
* @return {boolean} Whether the platform is iPad.
*/
goog.labs.userAgent.platform.isIpad = goog.memoize(
goog.partial(goog.labs.userAgent.util.matchUserAgent, 'iPad'));
/**
* @return {boolean} Whether the platform is iOS.
*/
goog.labs.userAgent.platform.isIos = goog.memoize(function() {
return goog.labs.userAgent.platform.isIphone() ||
goog.labs.userAgent.platform.isIpad() ||
goog.labs.userAgent.platform.isIpod();
});
/**
* @return {boolean} Whether the platform is Mac.
*/
goog.labs.userAgent.platform.isMac = goog.memoize(
goog.partial(goog.labs.userAgent.platform.matchPlatform_, 'Mac'));
/**
* @return {boolean} Whether the platform is Linux.
*/
goog.labs.userAgent.platform.isLinux = goog.memoize(
goog.partial(goog.labs.userAgent.platform.matchPlatform_, 'Linux'));
/**
* @return {boolean} Whether the platform is Windows.
*/
goog.labs.userAgent.platform.isWindows = goog.memoize(
goog.partial(goog.labs.userAgent.platform.matchPlatform_, 'Win'));
/**
* @return {boolean} Whether the platform is X11.
*/
goog.labs.userAgent.platform.isX11 = goog.memoize(function() {
var appVersion = goog.labs.userAgent.platform.getAppVersion();
return goog.string.contains(appVersion, 'X11');
});
/**
* @return {boolean} Whether the platform is ChromeOS.
*/
goog.labs.userAgent.platform.isChromeOS = goog.memoize(
goog.partial(goog.labs.userAgent.util.matchUserAgent, 'CrOS'));
/**
* The version of the platform. We only determine the version for Windows,
* Mac, and Chrome OS. It doesn't make much sense on Linux. For Windows, we only
* look at the NT version. Non-NT-based versions (e.g. 95, 98, etc.) are given
* version 0.0.
*
* @return {string} The platform version or empty string if version cannot be
* determined.
*/
goog.labs.userAgent.platform.getVersion = function() {
var userAgentString = goog.labs.userAgent.util.getUserAgentString();
var version = '', re;
if (goog.labs.userAgent.platform.isWindows()) {
re = /Windows NT ([0-9.]+)/;
var match = re.exec(userAgentString);
if (match) {
version = match[1];
} else {
version = '0.0';
}
} else if (goog.labs.userAgent.platform.isMac()) {
re = /Mac OS X ([0-9_.]+)/;
var match = re.exec(userAgentString);
// Note: some old versions of Camino do not report an OSX version.
// Default to 10.
version = match ? match[1].replace(/_/g, '.') : '10';
} else if (goog.labs.userAgent.platform.isAndroid()) {
re = /Android\s+([^\);]+)(\)|;)/;
var match = re.exec(userAgentString);
version = match && match[1];
} else if (goog.labs.userAgent.platform.isIos()) {
re = /(?:iPhone|iPod|iPad|CPU)\s+OS\s+(\S+)/;
var match = re.exec(userAgentString);
// Report the version as x.y.z and not x_y_z
version = match && match[1].replace(/_/g, '.');
} else if (goog.labs.userAgent.platform.isChromeOS()) {
re = /(?:CrOS\s+(?:i686|x86_64)\s+([0-9.]+))/;
var match = re.exec(userAgentString);
version = match && match[1];
}
return version || '';
};
/**
* @param {string|number} version The version to check.
* @return {boolean} Whether the browser version is higher or the same as the
* given version.
*/
goog.labs.userAgent.platform.isVersionOrHigher = function(version) {
return goog.string.compareVersions(goog.labs.userAgent.platform.getVersion(),
version) >= 0;
};

View File

@@ -0,0 +1,256 @@
// 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.userAgent.platform.
*/
goog.provide('goog.labs.userAgent.platformTest');
goog.require('goog.labs.userAgent.platform');
goog.require('goog.labs.userAgent.testAgents');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.userAgent.platformTest');
var propertyReplacer = new goog.testing.PropertyReplacer();
function setUp() {
// disable memoization
propertyReplacer.set(goog.memoize, 'ENABLE_MEMOIZE', false);
}
function tearDown() {
propertyReplacer.reset();
}
function setGlobalUAString(uaString, platform, appVersion) {
var mockGlobal = {
'navigator': {
'userAgent': uaString,
'platform': platform,
'appVersion': appVersion
}
};
propertyReplacer.set(goog, 'global', mockGlobal);
}
function testAndroid() {
var uaString = goog.labs.userAgent.testAgents.ANDROID_BROWSER_233;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isAndroid());
assertVersion('2.3.3');
assertVersionBetween('2.3.0', '2.3.5');
assertVersionBetween('2.3', '2.4');
assertVersionBetween('2', '3');
uaString = goog.labs.userAgent.testAgents.ANDROID_BROWSER_221;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isAndroid());
assertVersion('2.2.1');
assertVersionBetween('2.2.0', '2.2.5');
assertVersionBetween('2.2', '2.3');
assertVersionBetween('2', '3');
uaString = goog.labs.userAgent.testAgents.CHROME_ANDROID;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isAndroid());
assertVersion('4.0.2');
assertVersionBetween('4.0.0', '4.1.0');
assertVersionBetween('4.0', '4.1');
assertVersionBetween('4', '5');
}
function testIpod() {
var uaString = goog.labs.userAgent.testAgents.SAFARI_IPOD;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIpod());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('');
}
function testIphone() {
var uaString = goog.labs.userAgent.testAgents.SAFARI_IPHONE;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIphone());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('4.2.1');
assertVersionBetween('4', '5');
assertVersionBetween('4.2', '4.3');
uaString = goog.labs.userAgent.testAgents.IPHONE_6;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIphone());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('6.0');
assertVersionBetween('5', '7');
uaString = goog.labs.userAgent.testAgents.SAFARI_IPHONE_32;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIphone());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('3.2');
assertVersionBetween('3', '4');
}
function testIpad() {
var uaString = goog.labs.userAgent.testAgents.IPAD_4;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIpad());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('3.2');
assertVersionBetween('3', '4');
assertVersionBetween('3.1', '4');
uaString = goog.labs.userAgent.testAgents.IPAD_5;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIpad());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('5.1');
assertVersionBetween('5', '6');
uaString = goog.labs.userAgent.testAgents.IPAD_6;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isIpad());
assertTrue(goog.labs.userAgent.platform.isIos());
assertVersion('6.0');
assertVersionBetween('5', '7');
}
function testMac() {
var uaString = goog.labs.userAgent.testAgents.CHROME_MAC;
var platform = 'IntelMac';
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isMac());
assertVersion('10.8.2');
assertVersionBetween('10', '11');
assertVersionBetween('10.8', '10.9');
assertVersionBetween('10.8.1', '10.8.3');
uaString = goog.labs.userAgent.testAgents.OPERA_MAC;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isMac());
assertVersion('10.6.8');
assertVersionBetween('10', '11');
assertVersionBetween('10.6', '10.7');
assertVersionBetween('10.6.5', '10.7.0');
uaString = goog.labs.userAgent.testAgents.SAFARI_MAC;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isMac());
assertVersionBetween('10', '11');
assertVersionBetween('10.6', '10.7');
assertVersionBetween('10.6.5', '10.7.0');
uaString = goog.labs.userAgent.testAgents.FIREFOX_MAC;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isMac());
assertVersion('11.7.9');
assertVersionBetween('11', '12');
assertVersionBetween('11.7', '11.8');
assertVersionBetween('11.7.9', '11.8.0');
}
function testLinux() {
var uaString = goog.labs.userAgent.testAgents.FIREFOX_LINUX;
var platform = 'Linux';
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isLinux());
assertVersion('');
uaString = goog.labs.userAgent.testAgents.CHROME_LINUX;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isLinux());
assertVersion('');
uaString = goog.labs.userAgent.testAgents.OPERA_LINUX;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isLinux());
assertVersion('');
}
function testWindows() {
var uaString = goog.labs.userAgent.testAgents.SAFARI_WINDOWS;
var platform = 'Win32';
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isWindows());
assertVersion('6.1');
assertVersionBetween('6', '7');
uaString = goog.labs.userAgent.testAgents.IE_10;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isWindows());
assertVersion('6.1');
assertVersionBetween('6', '6.5');
uaString = goog.labs.userAgent.testAgents.CHROME_25;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isWindows());
assertVersion('5.1');
assertVersionBetween('5', '6');
uaString = goog.labs.userAgent.testAgents.FIREFOX_WINDOWS;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isWindows());
assertVersion('6.1');
assertVersionBetween('6', '7');
uaString = goog.labs.userAgent.testAgents.IE_11;
setGlobalUAString(uaString, platform);
assertTrue(goog.labs.userAgent.platform.isWindows());
assertVersion('6.3');
assertVersionBetween('6', '6.5');
}
function testX11() {
var uaString = goog.labs.userAgent.testAgents.CHROME_LINUX;
var platform = 'Linux';
var appVersion = goog.labs.userAgent.testAgents.CHROME_LINUX_APPVERVERSION;
setGlobalUAString(uaString, platform, appVersion);
assertTrue(goog.labs.userAgent.platform.isX11());
assertVersion('');
}
function testChromeOS() {
var uaString = goog.labs.userAgent.testAgents.CHROME_OS_910;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isChromeOS());
assertVersion('9.10.0');
assertVersionBetween('9', '10');
uaString = goog.labs.userAgent.testAgents.CHROME_OS;
setGlobalUAString(uaString);
assertTrue(goog.labs.userAgent.platform.isChromeOS());
assertVersion('3701.62.0');
assertVersionBetween('3701', '3702');
}
function assertVersion(version) {
assertEquals(version, goog.labs.userAgent.platform.getVersion());
}
function assertVersionBetween(lowVersion, highVersion) {
assertTrue(goog.labs.userAgent.platform.isVersionOrHigher(lowVersion));
assertFalse(goog.labs.userAgent.platform.isVersionOrHigher(highVersion));
}

View File

@@ -0,0 +1,272 @@
// 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 Various User-Agent strings.
* See http://go/useragentexamples and http://www.useragentstring.com/ for
* examples.
*
*/
goog.provide('goog.labs.userAgent.testAgents');
goog.setTestOnly('goog.labs.userAgent.testAgents');
goog.scope(function() {
var testAgents = goog.labs.userAgent.testAgents;
/** @const {string} */
testAgents.ANDROID_BROWSER_235 =
'Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; ' +
'HTC Vision Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) ' +
'Version/4.0 Mobile Safari/533.1';
/** @const {string} */
testAgents.ANDROID_BROWSER_221 =
'Mozilla/5.0 (Linux; U; Android 2.2.1; en-ca; LG-P505R Build/FRG83)' +
' AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1';
/** @const {string} */
testAgents.ANDROID_BROWSER_233 =
'Mozilla/5.0 (Linux; U; Android 2.3.3; en-us; HTC_DesireS_S510e' +
' Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0' +
' Mobile Safari/533.1';
/** @const {string} */
testAgents.ANDROID_BROWSER_403 =
'Mozilla/5.0 (Linux; U; Android 4.0.3; de-ch; HTC Sensation Build/IML74K)' +
' AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30';
/** @const {string} */
testAgents.IE_6 =
'Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1;' +
'.NET CLR 2.0.50727)';
/** @const {string} */
testAgents.IE_8 =
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)';
/** @const {string} */
testAgents.IE_8_COMPATIBILITY =
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0)';
/** @const {string} */
testAgents.IE_9 =
'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)';
/** @const {string} */
testAgents.IE_9_COMPATIBILITY =
'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)';
/** @const {string} */
testAgents.IE_10 =
'Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0;' +
' InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729;' +
' .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0';
/** @const {string} */
testAgents.IE_11 =
'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv 11.0) like Gecko';
/** @const {string} */
testAgents.FIREFOX_19 =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:19.0) ' +
'Gecko/20100101 Firefox/19.0';
/** @const {string} */
testAgents.FIREFOX_LINUX =
'Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101' +
' Firefox/15.0.1';
/** @const {string} */
testAgents.FIREFOX_MAC =
'Mozilla/6.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4)' +
' Gecko/2012010317 Firefox/10.0a4';
/** @const {string} */
testAgents.FIREFOX_WINDOWS =
'Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507' +
' Firefox/14.0.1';
/** @const {string} */
testAgents.SAFARI_6 =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_1) ' +
'AppleWebKit/536.25 (KHTML, like Gecko) ' +
'Version/6.0 Safari/536.25';
/** @const {string} */
testAgents.SAFARI_IPHONE =
'Mozilla/5.0 (iPhone; U; ru; CPU iPhone OS 4_2_1 like Mac OS X; ru)' +
' AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a' +
' Safari/6533.18.5';
/** @const {string} */
testAgents.SAFARI_IPHONE_431 =
'Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_1 like Mac OS X; zh-tw)' +
' AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8G4' +
' Safari/6533.18.5';
/** @const {string} */
testAgents.SAFARI_IPHONE_32 =
'Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us)' +
' AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314' +
' Safari/531.21.10';
/** @const {string} */
testAgents.SAFARI_IPOD =
'Mozila/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1' +
' (KHTML, like Gecko) Version/3.0 Mobile/3A101a Safari/419.3';
/** @const {string} */
testAgents.SAFARI_MAC =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+' +
' (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2';
/** @const {string} */
testAgents.SAFARI_WINDOWS =
'Mozilla/5.0 (Windows; U; Windows NT 6.1; tr-TR) AppleWebKit/533.20.25' +
' (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27';
/** @const {string} */
testAgents.OPERA_10 =
'Opera/9.80 (S60; SymbOS; Opera Mobi/447; U; en) ' +
'Presto/2.4.18 Version/10.00';
/** @const {string} */
testAgents.OPERA_LINUX =
'Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50';
/** @const {string} */
testAgents.OPERA_MAC =
'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168' +
' Version/11.52';
/** @const {string} */
testAgents.IPHONE_6 =
'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 ' +
'like Mac OS X) AppleWebKit/536.26 ' +
'(KHTML, like Gecko) Mobile/10A5376e';
/** @const {string} */
testAgents.IPAD_4 =
'Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us)' +
' AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b' +
' Safari/531.21.10';
/** @const {string} */
testAgents.IPAD_5 =
'Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X; en-us) AppleWebKit/534.46' +
' (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3';
/** @const {string} */
testAgents.IPAD_6 =
'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) ' +
'AppleWebKit/536.26 (KHTML, like Gecko) ' +
'Version/6.0 Mobile/10A403 Safari/8536.25';
/** @const {string} */
testAgents.CHROME_25 =
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) ' +
'AppleWebKit/535.8 (KHTML, like Gecko) ' +
'Chrome/25.0.1000.10 Safari/535.8';
/** @const {string} */
testAgents.CHROME_ANDROID =
'Mozilla/5.0 (Linux; U; Android 4.0.2; en-us; Galaxy Nexus Build/ICL53F) ' +
'AppleWebKit/535.7 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile ' +
'Safari/535.7';
/** @const {string} */
testAgents.CHROME_ANDROID_TABLET =
'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) ' +
'AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Safari/535.19';
/** @const {string} */
testAgents.CHROME_IPHONE =
'Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X; en-us) ' +
'AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/22.0.1194.0 Mobile/11E53 ' +
'Safari/7534.48.3';
/** @const {string} */
testAgents.CHROME_LINUX =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko)' +
' Chrome/26.0.1410.33 Safari/537.31';
/**
* We traditionally use Appversion to detect X11
* @const {string}
*/
testAgents.CHROME_LINUX_APPVERVERSION =
'5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko)' +
' Chrome/26.0.1410.33 Safari/537.31';
/** @const {string} */
testAgents.CHROME_MAC =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17' +
' (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17';
/** @const {string} */
testAgents.CHROME_OS =
'Mozilla/5.0 (X11; CrOS x86_64 3701.62.0) AppleWebKit/537.31 ' +
'(KHTML, like Gecko) Chrome/26.0.1410.40 Safari/537.31';
/** @const {string} */
testAgents.CHROME_OS_910 =
'Mozilla/5.0 (X11; U; CrOS i686 9.10.0; en-US) AppleWebKit/532.5' +
' (KHTML, like Gecko) Chrome/4.0.253.0 Safari/532.5';
/** @const {string} */
testAgents.KINDLE_FIRE =
'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; KFTT Build/IML74K)' +
' AppleWebKit/535.19 (KHTML, like Gecko) Silk/2.1 Mobile Safari/535.19' +
' Silk-Accelerated=true';
}); // goog.scope

View File

@@ -0,0 +1,89 @@
// 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 used by goog.labs.userAgent tools. These functions
* should not be used outside of goog.labs.userAgent.*.
*
* @visibility {//visibility:private}
* @author nnaze@google.com (Nathan Naze)
*/
goog.provide('goog.labs.userAgent.util');
goog.require('goog.memoize');
goog.require('goog.string');
/**
* Returns the user agent string.
*
* @return {string} The user agent string.
*/
goog.labs.userAgent.util.getUserAgentString = goog.memoize(function() {
return goog.global['navigator'] ? goog.global['navigator'].userAgent : '';
});
/**
* @param {string} str
* @return {boolean} Whether the user agent contains the given string.
*/
goog.labs.userAgent.util.matchUserAgent = function(str) {
var userAgentString = goog.labs.userAgent.util.getUserAgentString();
return goog.string.contains(userAgentString, str);
};
/**
* Parses the user agent into tuples for each section.
* @param {string} userAgent
* @return {!Array.<string>} Tuples of key, version, and the contents of the
* parenthetical.
*/
goog.labs.userAgent.util.extractVersionTuples = function(userAgent) {
// Matches each section of a user agent string.
// Example UA:
// Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)
// AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405
// This has three version tuples: Mozilla, AppleWebKit, and Mobile.
var versionRegExp = new RegExp(
// Key. Note that a key may have a space.
// (i.e. 'Mobile Safari' in 'Mobile Safari/5.0')
'(\\w[\\w ]+)' +
'/' + // slash
'([^\\s]+)' + // version (i.e. '5.0b')
'\\s*' + // whitespace
'(?:\\((.*?)\\))?', // parenthetical info. parentheses not matched.
'g');
var data = [];
var match;
// Iterate and collect the version tuples. Each iteration will be the
// next regex match.
while (match = versionRegExp.exec(userAgent)) {
data.push([
match[1], // key
match[2], // value
// || undefined as this is not undefined in IE7 and IE8
match[3] || undefined // info
]);
}
return data;
};

View File

@@ -0,0 +1,60 @@
// 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 frexcept 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.userAgent.engine.
*/
goog.provide('goog.labs.userAgent.utilTest');
goog.require('goog.labs.userAgent.testAgents');
goog.require('goog.labs.userAgent.util');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.labs.userAgent.utilTest');
/**
* Tests parsing a few example UA strings.
*/
function testExtractVersionTuples() {
// Old Android
var tuples = goog.labs.userAgent.util.extractVersionTuples(
goog.labs.userAgent.testAgents.ANDROID_BROWSER_235);
assertEquals(4, tuples.length);
assertSameElements(
['Mozilla', '5.0',
'Linux; U; Android 2.3.5; en-us; HTC Vision Build/GRI40'], tuples[0]);
assertSameElements(['AppleWebKit', '533.1', 'KHTML, like Gecko'], tuples[1]);
assertSameElements(['Version', '4.0', undefined], tuples[2]);
assertSameElements(['Mobile Safari', '533.1', undefined], tuples[3]);
// IE 9
tuples = goog.labs.userAgent.util.extractVersionTuples(
goog.labs.userAgent.testAgents.IE_9);
assertEquals(1, tuples.length);
assertSameElements(
['Mozilla', '5.0',
'compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0'], tuples[0]);
// Opera
tuples = goog.labs.userAgent.util.extractVersionTuples(
goog.labs.userAgent.testAgents.OPERA_10);
assertEquals(3, tuples.length);
assertSameElements(['Opera', '9.80', 'S60; SymbOS; Opera Mobi/447; U; en'],
tuples[0]);
assertSameElements(['Presto', '2.4.18', undefined], tuples[1]);
assertSameElements(['Version', '10.00', undefined], tuples[2]);
}