Merge pull request #151 from acdha/overhauled-ajax-error-reporting

Overhauled AJAX error reporting
This commit is contained in:
iangilman 2013-07-03 09:56:07 -07:00
commit 850aa14802
12 changed files with 399 additions and 175 deletions

View File

@ -882,47 +882,49 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){
/**
* Wraps the given element in a nest of divs so that the element can
* be easily centered.
* be easily centered using CSS tables
* @function
* @name OpenSeadragon.makeCenteredNode
* @param {Element|String} element
* @returns {Element}
* @returns {Element} outermost wrapper element
*/
makeCenteredNode: function( element ) {
var div = $.makeNeutralElement( "div" ),
html = [],
innerDiv,
innerDivs;
// Convert a possible ID to an actual HTMLElement
element = $.getElement( element );
//TODO: I dont understand the use of # inside the style attributes
// below. Invetigate the results of the constructed html in
// the browser and clean up the mark-up to make this clearer.
html.push('<div style="display:table; height:100%; width:100%;');
html.push('border:none; margin:0px; padding:0px;'); // neutralizing
html.push('#position:relative; overflow:hidden; text-align:left;">');
html.push('<div style="#position:absolute; #top:50%; width:100%; ');
html.push('border:none; margin:0px; padding:0px;'); // neutralizing
html.push('display:table-cell; vertical-align:middle;">');
html.push('<div style="#position:relative; #top:-50%; width:100%; ');
html.push('border:none; margin:0px; padding:0px;'); // neutralizing
html.push('text-align:center;"></div></div></div>');
/*
CSS tables require you to have a display:table/row/cell hierarchy so we need to create
three nested wrapper divs:
*/
div.innerHTML = html.join( '' );
div = div.firstChild;
var wrappers = [
$.makeNeutralElement( 'div' ),
$.makeNeutralElement( 'div' ),
$.makeNeutralElement( 'div' )
];
innerDiv = div;
innerDivs = div.getElementsByTagName( "div" );
while ( innerDivs.length > 0 ) {
innerDiv = innerDivs[ 0 ];
innerDivs = innerDiv.getElementsByTagName( "div" );
}
// It feels like we should be able to pass style dicts to makeNeutralElement:
$.extend(wrappers[0].style, {
display: "table",
height: "100%",
width: "100%"
});
innerDiv.appendChild( element );
$.extend(wrappers[1].style, {
display: "table-row"
});
return div;
$.extend(wrappers[2].style, {
display: "table-cell",
verticalAlign: "middle",
textAlign: "center"
});
wrappers[0].appendChild(wrappers[1]);
wrappers[1].appendChild(wrappers[2]);
wrappers[2].appendChild(element);
return wrappers[0];
},
@ -1313,7 +1315,7 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){
makeAjaxRequest: function( url, onSuccess, onError ) {
var request = $.createAjaxRequest();
if (!$.isFunction(onSuccess)) {
if ( !$.isFunction( onSuccess ) ) {
throw new Error( "makeAjaxRequest requires a success callback" );
}
@ -1325,9 +1327,9 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){
if ( request.status == 200 ) {
onSuccess( request );
} else {
$.console.log("AJAX request returned %s: %s", request.status, url);
$.console.log( "AJAX request returned %s: %s", request.status, url );
if ($.isFunction(onError)) {
if ( $.isFunction( onError ) ) {
onError( request );
}
}
@ -1338,11 +1340,11 @@ window.OpenSeadragon = window.OpenSeadragon || function( options ){
request.open( "GET", url, true );
request.send( null );
} catch (e) {
$.console.log("%s while making AJAX request: %s", e.name, e.message);
$.console.log( "%s while making AJAX request: %s", e.name, e.message );
request.onreadystatechange = function(){};
if ($.isFunction(onError)) {
if ( $.isFunction( onError ) ) {
onError( request, e );
}
}

View File

@ -45,7 +45,8 @@ var I18N = {
ImageFormat: "Sorry, we don't support {0}-based Deep Zoom Images.",
Security: "It looks like a security restriction stopped us from " +
"loading this Deep Zoom Image.",
Status: "This space unintentionally left blank ({0} {1})."
Status: "This space unintentionally left blank ({0} {1}).",
"Open-Failed": "Unable to open {0}: {1}"
},
Tooltips: {
@ -80,7 +81,8 @@ $.extend( $, {
string = container[ props[ i ] ];
if ( typeof( string ) != "string" ) {
string = "";
$.console.debug( "Untranslated source string:", prop );
string = ""; // FIXME: this breaks gettext()-style convention, which would return source
}
return string.replace(/\{\d+\}/g, function(capture) {

View File

@ -293,6 +293,11 @@ $.TileSource.prototype = {
callback = function( data ){
var $TileSource = $.TileSource.determineType( _this, data, url );
if ( !$TileSource ) {
_this.raiseEvent( 'open-failed', { message: "Unable to load TileSource", source: url } );
return;
}
options = $TileSource.prototype.configure.apply( _this, [ data, url ]);
readySource = new $TileSource( options );
_this.ready = true;
@ -315,6 +320,11 @@ $.TileSource.prototype = {
$.makeAjaxRequest( url, function( xhr ) {
var data = processResponse( xhr );
callback( data );
}, function ( xhr ) {
_this.raiseEvent( 'open-failed', {
message: "HTTP " + xhr.status + " attempting to load TileSource",
source: url
});
});
}
@ -458,6 +468,8 @@ $.TileSource.determineType = function( tileSource, data, url ){
return OpenSeadragon[ property ];
}
}
$.console.error( "No TileSource was able to open %s %s", url, data );
};

View File

@ -171,6 +171,12 @@ $.Viewer = function( options ) {
//Inherit some behaviors and properties
$.EventHandler.call( this );
this.addHandler( 'open-failed', function (source, args) {
var msg = $.getString( "Errors.Open-Failed", args.source, args.message);
_this._showMessage( msg );
});
$.ControlDock.call( this, options );
//Deal with tile sources
@ -417,6 +423,8 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype,
$TileSource,
options;
_this._hideMessage();
//allow plain xml strings or json strings to be parsed here
if( $.type( tileSource ) == 'string' ){
if( tileSource.match(/\s*<.*/) ){
@ -433,6 +441,9 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype,
tileSource = new $.TileSource( tileSource, function( readySource ){
openTileSource( _this, readySource );
});
tileSource.addHandler( 'open-failed', function ( name, args ) {
_this.raiseEvent( 'open-failed', args );
});
} else if ( $.isPlainObject( tileSource ) || tileSource.nodeType ){
if( $.isFunction( tileSource.getTileUrl ) ){
@ -443,6 +454,13 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype,
} else {
//inline configuration
$TileSource = $.TileSource.determineType( _this, tileSource );
if ( !$TileSource ) {
_this.raiseEvent( 'open-failed', {
message: "Unable to load TileSource",
source: tileSource
});
return;
}
options = $TileSource.prototype.configure.apply( _this, [ tileSource ]);
readySource = new $TileSource( options );
openTileSource( _this, readySource );
@ -1054,8 +1072,39 @@ $.extend( $.Viewer.prototype, $.EventHandler.prototype, $.ControlDock.prototype,
this.referenceStrip.setFocus( page );
}
return this;
}
},
/**
* Display a message in the viewport
* @function
* @private
* @param {String} text message
*/
_showMessage: function ( message ) {
this._hideMessage();
var div = $.makeNeutralElement( "div" );
div.appendChild( document.createTextNode( message ) );
this.messageDiv = $.makeCenteredNode( div );
$.addClass(this.messageDiv, "openseadragon-message");
this.container.appendChild( this.messageDiv );
},
/**
* Hide any currently displayed viewport message
* @function
* @private
*/
_hideMessage: function () {
var div = this.messageDiv;
if (div) {
div.parentNode.removeChild(div);
delete this.messageDiv;
}
}
});
/**

View File

@ -1,23 +1,31 @@
/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util */
/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */
(function() {
var viewer;
module('Basic');
module('Basic', {
setup: function () {
var example = $('<div id="example"></div>').appendTo("#qunit-fixture");
// TODO: Test drag
testLog.reset();
var viewer = null;
// ----------
asyncTest('Open', function() {
$(document).ready(function() {
viewer = OpenSeadragon({
id: 'example',
prefixUrl: '/build/openseadragon/images/',
tileSources: '/test/data/testpattern.dzi',
springStiffness: 100 // Faster animation = faster tests
});
},
teardown: function () {
if (viewer && viewer.close) {
viewer.close();
}
viewer = null;
}
});
// ----------
asyncTest('Open', function() {
ok(viewer, 'Viewer exists');
var openHandler = function(eventSender, eventData) {
@ -32,12 +40,36 @@
};
viewer.addHandler('open', openHandler);
viewer.open('/test/data/testpattern.dzi');
});
asyncTest('Open Error Handling', function() {
ok(viewer, 'Viewer exists');
viewer.addHandler('open', function(eventSender, eventData) {
ok(false, "The open event should not fire for failed opens");
start();
});
viewer.addHandler('open-failed', function(eventSender, eventData) {
ok(true, "The open-failed event should be fired when the source 404s");
equal($(".openseadragon-message").length, 1, "Open failures should display a message");
ok(testLog.log.contains('["AJAX request returned %s: %s",404,"/test/data/not-a-real-file"]'),
"AJAX failures should be logged to the console");
start();
});
viewer.open('/test/data/not-a-real-file');
});
// ----------
asyncTest('Zoom', function() {
viewer.addHandler("open", function () {
var viewport = viewer.viewport;
equal(viewport.getZoom(), 1, 'We start out unzoomed');
var zoomHandler = function() {
@ -49,11 +81,15 @@
viewer.addHandler('animationfinish', zoomHandler);
viewport.zoomTo(2);
});
viewer.open('/test/data/testpattern.dzi');
});
// ----------
asyncTest('Pan', function() {
var viewport = viewer.viewport;
var center = viewport.getCenter();
viewer.addHandler("open", function () {
var viewport = viewer.viewport,
center = viewport.getCenter();
ok(center.x === 0.5 && center.y === 0.5, 'We start out unpanned');
var panHandler = function() {
@ -67,10 +103,24 @@
viewport.panTo(new OpenSeadragon.Point(0.1, 0.1));
});
viewer.open('/test/data/testpattern.dzi');
});
// ----------
asyncTest('Home', function() {
// Test setup:
function opener() {
var viewport = viewer.viewport;
var center = viewport.getCenter();
viewport.panTo(new OpenSeadragon.Point(0.1, 0.1));
viewport.zoomTo(2);
}
function stage1() {
var viewport = viewer.viewport,
center = viewport.getCenter();
viewer.removeHandler('animationfinish', stage1);
ok(center.x !== 0.5 && center.y !== 0.5, 'We start out panned');
notEqual(viewport.getZoom(), 1, 'We start out zoomed');
@ -84,10 +134,17 @@
viewer.addHandler('animationfinish', homeHandler);
viewport.goHome(true);
}
viewer.addHandler("open", opener);
viewer.addHandler("animationfinish", stage1);
viewer.open('/test/data/testpattern.dzi');
});
// ----------
asyncTest('Click', function() {
viewer.addHandler("open", function () {
var viewport = viewer.viewport,
center = viewport.getCenter();
@ -106,8 +163,12 @@
Util.simulateViewerClick(viewer, 0.25, 0.25);
});
viewer.open('/test/data/testpattern.dzi');
});
// ----------
test('Fullscreen', function() {
asyncTest('Fullscreen', function() {
viewer.addHandler("open", function () {
ok(!viewer.isFullPage(), 'Started out not fullpage');
ok(!$(viewer.element).hasClass('fullpage'),
'No fullpage class on div');
@ -121,14 +182,18 @@
ok(!viewer.isFullPage(), 'Disabled fullpage');
ok(!$(viewer.element).hasClass('fullpage'),
'Fullpage class removed from div');
start();
});
viewer.open('/test/data/testpattern.dzi');
});
// ----------
asyncTest('Close', function() {
viewer.addHandler("open", function () {
var closeHandler = function() {
viewer.removeHandler('close', closeHandler);
ok(!viewer.source, 'no source');
$('#example').empty();
ok(true, 'Close event was sent');
ok(!viewer._updateRequestId, 'timer is off');
setTimeout(function() {
@ -140,5 +205,7 @@
viewer.addHandler('close', closeHandler);
viewer.close();
});
viewer.open('/test/data/testpattern.dzi');
});
})();

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>
<head>
<title>OpenSeadragon Basic Demo</title>

View File

@ -3,7 +3,13 @@
// This module tests whether our various file formats can be opened.
// TODO: Add more file formats (with corresponding test data).
module('Formats');
module('Formats', {
setup: function () {
var example = document.createElement("div");
example.id = "example";
document.getElementById("qunit-fixture").appendChild(example);
}
});
var viewer = null;

View File

@ -1,3 +1,5 @@
/* global QUnit, module, Util, $, console, test, asyncTest, start, ok, equal */
QUnit.config.autostart = false;
(function () {
@ -14,14 +16,18 @@ QUnit.config.autostart = false;
topNavigatorClickAdjustment;
module("navigator", {
setup:function () {
Util.resetDom();
setup: function () {
Util.initializeTestDOM();
resetTestVariables();
$(document).scrollTop(0);
$(document).scrollLeft(0);
},
teardown:function () {
Util.resetDom();
teardown: function () {
// jQuery UI creates its controls outside the normal DOM hierarchy which QUnit cleans up:
if ($('#exampleNavigator').is(':ui-dialog')) {
$('#exampleNavigator').dialog('destroy');
}
resetTestVariables();
}
});
@ -31,7 +37,7 @@ QUnit.config.autostart = false;
});
var resetTestVariables = function () {
if (viewer != null) {
if (viewer) {
viewer.close();
}
displayRegion = null;
@ -125,11 +131,11 @@ QUnit.config.autostart = false;
viewerAndNavigatorDisplayReady = viewer.drawer !== null &&
!viewer.drawer.needsUpdate() &&
currentDisplayWidth > 0 &&
Util.equalsWithVariance(lastDisplayRegionLeft, currentDisplayRegionLeft, .0001) &&
Util.equalsWithVariance(lastDisplayWidth, currentDisplayWidth, .0001) &&
Util.equalsWithVariance(viewer.viewport.getBounds(true).x, viewer.viewport.getBounds().x, .0001) &&
Util.equalsWithVariance(viewer.viewport.getBounds(true).y, viewer.viewport.getBounds().y, .0001) &&
Util.equalsWithVariance(viewer.viewport.getBounds(true).width, viewer.viewport.getBounds().width, .0001);
Util.equalsWithVariance(lastDisplayRegionLeft, currentDisplayRegionLeft, 0.0001) &&
Util.equalsWithVariance(lastDisplayWidth, currentDisplayWidth, 0.0001) &&
Util.equalsWithVariance(viewer.viewport.getBounds(true).x, viewer.viewport.getBounds().x, 0.0001) &&
Util.equalsWithVariance(viewer.viewport.getBounds(true).y, viewer.viewport.getBounds().y, 0.0001) &&
Util.equalsWithVariance(viewer.viewport.getBounds(true).width, viewer.viewport.getBounds().width, 0.0001);
}
catch (err) {
//Ignore. Subsequent code will try again shortly
@ -138,7 +144,7 @@ QUnit.config.autostart = false;
count++;
setTimeout(function () {
waitForViewer(handler, count, currentDisplayRegionLeft, currentDisplayWidth);
}, 100)
}, 100);
}
else {
if (count === 40) {
@ -201,21 +207,21 @@ QUnit.config.autostart = false;
expecteYCoordinate = 1 / viewer.source.aspectRatio - viewer.viewport.getBounds().height;
}
if (viewer.viewport.getBounds().width < 1) {
Util.assessNumericValue(expectedXCoordinate, viewer.viewport.getBounds().x, .04, ' Viewer at ' + theContentCorner + ', x coord');
Util.assessNumericValue(expectedXCoordinate, viewer.viewport.getBounds().x, 0.04, ' Viewer at ' + theContentCorner + ', x coord');
}
if (viewer.viewport.getBounds().height < 1 / viewer.source.aspectRatio) {
Util.assessNumericValue(expecteYCoordinate, viewer.viewport.getBounds().y, .04, ' Viewer at ' + theContentCorner + ', y coord');
}
Util.assessNumericValue(expecteYCoordinate, viewer.viewport.getBounds().y, 0.04, ' Viewer at ' + theContentCorner + ', y coord');
}
};
};
var assessViewerInCenter = function () {
var yPositionVariance = .04;
var yPositionVariance = 0.04;
if (viewer.source.aspectRatio < 1) {
yPositionVariance = yPositionVariance / viewer.source.aspectRatio;
}
Util.assessNumericValue(1 / viewer.source.aspectRatio / 2, viewer.viewport.getCenter().y, yPositionVariance, ' Viewer at center, y coord');
Util.assessNumericValue(.5, viewer.viewport.getCenter().x, .4, ' Viewer at center, x coord');
Util.assessNumericValue(0.5, viewer.viewport.getCenter().x, 0.4, ' Viewer at center, x coord');
};
var clickOnNavigator = function (theContentCorner) {
@ -239,7 +245,7 @@ QUnit.config.autostart = false;
yPos = contentStartFromTop + displayRegionHeight;
}
simulateNavigatorClick(viewer.navigator, xPos, yPos);
}
};
};
var dragNavigatorBackToCenter = function () {
@ -354,11 +360,12 @@ QUnit.config.autostart = false;
clientX:1,
clientY:1
};
mainViewerElement.simulate('blur', event);
$("#" + seadragonProperties.id).simulate('blur', event);
if (testProperties.expectedAutoFade) {
setTimeout(assessAutoFadeTriggered,autoFadeWaitTime);
}
else {
} else {
setTimeout(assessAutoFadeDisabled,autoFadeWaitTime);
}
}

View File

@ -1,17 +1,33 @@
/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */
(function() {
module("strings");
module("strings", {
setup: function () {
testLog.reset();
}
});
test("getSubString", function() {
equal(OpenSeadragon.getString("Errors.Dzi"),
"Hmm, this doesn't appear to be a valid Deep Zoom Image.",
"Read sub-string");
});
test("getStringWithPlaceholders", function() {
equal(OpenSeadragon.getString("Errors.Open-Failed", "foo", "bar"),
"Unable to open foo: bar",
"String placeholder replacement");
});
test("getInvalidString", function() {
equal(OpenSeadragon.getString("Greeting"), "",
"Handled unset string key");
equal(OpenSeadragon.getString("Errors"), "",
"Handled requesting parent key");
equal(OpenSeadragon.getString("Greeting"), "", "Handled unset string key");
ok(testLog.debug.contains('["Untranslated source string:","Greeting"]'),
'Invalid string keys are logged');
equal(OpenSeadragon.getString("Errors"), "", "Handled requesting parent key");
ok(testLog.debug.contains('["Untranslated source string:","Errors"]'),
'Invalid string parent keys are logged');
});
test("setString", function() {

View File

@ -10,12 +10,6 @@
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<div>
<div id="example"></div>
<div id="exampleNavigator"></div>
</div>
<div id="wideexample"></div>
<div id="tallexample"></div>
<script src="/node_modules/grunt-contrib-qunit/test/libs/qunit.js"></script>
<script src="/test/lib/jquery-1.9.1.min.js"></script>
<script src="/test/lib/jquery-ui-1.10.2/js/jquery-ui-1.10.2.min.js"></script>

View File

@ -1,3 +1,5 @@
/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util */
(function() {
// ----------
@ -30,16 +32,11 @@
.simulate('mouseup', event);
},
resetDom: function () {
if ($('#exampleNavigator').is(':ui-dialog')) {
$('#exampleNavigator').dialog('destroy');
}
$("#exampleNavigator").remove();
$(".navigator").remove();
$("#example").empty();
$("#tallexample").empty();
$("#wideexample").empty();
$("#example").parent().append('<div id="exampleNavigator"></div>');
initializeTestDOM: function () {
$("#qunit-fixture")
.append('<div><div id="example"></div><div id="exampleNavigator"></div></div>')
.append('<div id="wideexample"></div>')
.append('<div id="tallexample"></div>');
},
equalsWithVariance: function (value1, value2, variance) {
@ -74,5 +71,51 @@
};
/*
Test console log capture
1. Only the OpenSeadragon.console logger is touched
2. All log messages are stored in window.testLog in arrays keyed on the logger name (e.g. log,
warning, error, etc.) as JSON-serialized strings to simplify comparisons
3. The captured log arrays have a custom contains() method for ease of testing
4. testLog.reset() will clear all of the message arrays, intended for use in test setup routines
*/
var testConsole = window.testConsole = {},
testLog = window.testLog = {
log: [],
debug: [],
info: [],
warn: [],
error: [],
reset: function () {
for (var i in testLog) {
if (testLog.hasOwnProperty(i) && 'length' in testLog[i] && 'push' in testLog[i]) {
testLog[i].length = 0;
}
}
}
};
for (var i in testLog) {
if (testLog.hasOwnProperty(i) && testLog[i].push) {
testConsole[i] = (function (arr) {
return function () {
var args = Array.prototype.slice.call(arguments, 0); // Coerce to true Array
arr.push(JSON.stringify(args)); // Store as JSON to avoid tedious array-equality tests
};
})(testLog[i]);
testLog[i].contains = function (needle) {
for (var i = 0; i < this.length; i++) {
if (this[i] == needle) {
return true;
}
}
return false;
};
}
}
OpenSeadragon.console = testConsole;
})();

View File

@ -1,3 +1,5 @@
/* global module, asyncTest, $, ok, equal, strictEqual, notEqual, start, test, Util, testLog */
(function() {
module("utils");
@ -56,10 +58,33 @@
asyncTest("makeAjaxRequest", function() {
var timeWatcher = Util.timeWatcher();
OpenSeadragon.makeAjaxRequest('data/testpattern.dzi', function(xhr) {
ok(/deepzoom/.test(xhr.response), 'file loaded');
OpenSeadragon.makeAjaxRequest('data/testpattern.dzi',
function(xhr) {
equal(xhr.status, 200, 'Success callback called for HTTP 200');
ok(/deepzoom/.test(xhr.responseText), 'Success function called');
timeWatcher.done();
},
function(xhr) {
ok(false, 'Error callback should not be called');
timeWatcher.done();
}
);
});
asyncTest("makeAjaxRequest for invalid file", function() {
var timeWatcher = Util.timeWatcher();
OpenSeadragon.makeAjaxRequest('not-a-real-dzi-file',
function(xhr) {
ok(false, 'Success function should not be called for errors');
timeWatcher.done();
},
function(xhr) {
equal(xhr.status, 404, 'Error callback called for HTTP 404');
ok(true, 'Error function should be called for errors');
timeWatcher.done();
}
);
});
// ----------