blob: 42d3fc69e34deffdd24eea37be56b644673ff595 [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Class to track the progress events received by a particular plugin instance.
function EventStateMachine() {
// Work around how JS binds 'this'.
var this_ = this;
// Given a particular state, what are the acceptable event types.
this.expectedNext = {
'BEGIN': { 'loadstart': 1 },
'loadstart': { 'progress': 1, 'error': 1, 'abort': 1, 'load': 1 },
'progress': { 'progress': 1, 'error': 1, 'abort': 1, 'load': 1 },
'error': { 'loadend': 1 },
'abort': { 'loadend': 1 },
'load': { 'loadend': 1 },
'loadend': { },
'UNEXPECTED': { },
};
// The current state (and index into expectedNext).
this.currentState = 'BEGIN';
// For each recognized state, a count of the times it was reached.
this.stateHistogram = {
'BEGIN': 0,
'loadstart': 0,
'progress': 0,
'error': 0,
'abort': 0,
'load': 0,
'loadend': 0,
'UNEXPECTED': 0
};
// The state transition function.
this.transitionTo = function(event_type) {
// The index values of this_.expectedNext are the only valid states.
// Invalid event types are normalized to 'UNEXPECTED'.
if (this_.expectedNext[event_type] == undefined) {
console.log('unexpected ' + event_type);
event_type = 'UNEXPECTED';
}
// Check that the next event type is expected from the current state.
// If not, we transition to the state 'UNEXPECTED'.
if (!(event_type in this_.expectedNext[this_.currentState])) {
console.log('unexpected ' + event_type + ' from ' + this_.currentState);
event_type = 'UNEXPECTED';
}
this_.currentState = event_type;
this_.stateHistogram[this_.currentState]++;
}
// True if an event with lengthComputable is ever triggered.
this.stateSawLengthComputable = false;
// The last event.total seen from an event with lengthComputable being true.
this.stateProgressTotal = -1;
// The last event.loaded seen from an event with lengthComputable being true.
this.stateProgressPrev = -1;
// Function to record progress stats.
this.recordProgress = function(event) {
// Can either record progress from a progress event with lengthComputable,
// or from a loadend event.
if (event.type == 'progress' && event.lengthComputable) {
this.stateSawLengthComputable = true;
this.stateProgressTotal = event.total;
this.stateProgressPrev = event.loaded;
} else if (event.type == 'loadend' && event.lengthComputable) {
this.stateProgressTotal = event.total;
this.stateProgressPrev = event.loaded;
}
}
}
// event_machines is a collection of EventStateMachines, one for each element
// id that dispatches an event of a type we are listening for.
window.event_machines = { };
// Look up the EventStateMachine for the id.
function lookupEventMachine(element_id) {
var event_machine = window.event_machines[element_id];
if (event_machine == undefined) {
// This is the first event for this target. Create an EventStateMachine.
event_machine = new EventStateMachine();
window.event_machines[element_id] = event_machine;
}
return event_machine;
}
// Sets up event listeners on the body element for all the progress
// event types. Delegation to the body allows this to be done only once
// per document.
var setListeners = function(body_element) {
var eventListener = function(e) {
// Find the target element of the event.
var target_element = e.target;
// Body only dispatches for elements having the 'naclModule' CSS class.
if (target_element.className != 'naclModule') {
return;
}
var element_id = target_element.id;
// Look up the EventStateMachine for the target of the event.
var event_machine = lookupEventMachine(element_id);
// Update the state of the machine.
event_machine.transitionTo(e.type);
// Record progress information if possible.
event_machine.recordProgress(e);
}
// Add the listener for all of the ProgressEvent event types.
body_element.addEventListener('loadstart', eventListener, true);
body_element.addEventListener('progress', eventListener, true);
body_element.addEventListener('error', eventListener, true);
body_element.addEventListener('abort', eventListener, true);
body_element.addEventListener('load', eventListener, true);
body_element.addEventListener('loadend', eventListener, true);
}
// Performs some tests to make sure that progress events follow the expected
// state transitions to end in an expected state.
function testProgressEventStateMachine(tester,
embedId,
progressMinCount,
errorCount,
abortCount,
loadCount,
lastError) {
var eventMachine = lookupEventMachine(embedId);
// Test the expected number of occurrences, with some duplication.
tester.addTest('begin_count_' + embedId, function() {
// There should be no 'BEGIN' event.
assertEqual(eventMachine.stateHistogram['BEGIN'], 0);
});
tester.addTest('loadstart_count_' + embedId, function() {
// There should be one 'loadstart' event.
assertEqual(eventMachine.stateHistogram['loadstart'], 1);
});
tester.addTest('progress_min_count_' + embedId, function() {
// There should be at least one progress event when the manifest file is
// loaded and another when the .nexe is loaded.
assert(eventMachine.stateHistogram['progress'] >= progressMinCount);
});
tester.addTest('progress_samples_' + embedId, function() {
console.log('stateSawLengthComputable ' +
eventMachine.stateSawLengthComputable);
console.log('stateProgressPrev ' +
eventMachine.stateProgressPrev);
console.log('stateProgressTotal ' +
eventMachine.stateProgressTotal);
assert(eventMachine.stateSawLengthComputable);
// Progress events are not necessarily monotonic. For glibc, each DSO
// will trigger a different series of progress events with different totals.
// For glibc, the final loadend progress event may even correspond to
// the very first load event, instead of corresponding to the last...
// So, all we check is that the latest values make some sense.
assert(eventMachine.stateProgressPrev > 0);
assert(eventMachine.stateProgressTotal > 0);
assert(eventMachine.stateProgressPrev <= eventMachine.stateProgressTotal);
});
tester.addTest('error_count_' + embedId, function() {
// Check that the right number of 'error' events were dispatched.
assertEqual(eventMachine.stateHistogram['error'], errorCount);
});
tester.addTest('abort_count_' + embedId, function() {
// Check that the right number of 'abort' events were dispatched.
assertEqual(eventMachine.stateHistogram['abort'], abortCount);
});
tester.addTest('load_count_' + embedId, function() {
// Check that the right number of 'load' events were dispatched.
assertEqual(eventMachine.stateHistogram['load'], loadCount);
})
tester.addTest('loadend_count_' + embedId, function() {
// There should be one 'loadend' event.
assertEqual(eventMachine.stateHistogram['loadend'], 1);
});
tester.addTest('unexpected_count_' + embedId, function() {
// There should be no 'UNEXPECTED' event.
assertEqual(eventMachine.stateHistogram['UNEXPECTED'], 0);
});
tester.addTest('end_state_' + embedId, function() {
// Test that the progress events followed the expected sequence to
// completion in the 'loadend' state.
assertEqual(eventMachine.currentState, 'loadend');
});
tester.addTest('last_error_string_' + embedId, function() {
// If an error or abort was reported, check that lastError is set
// to the correct value.
if ((eventMachine.stateHistogram['error'] > 0 ||
eventMachine.stateHistogram['abort'] > 0)) {
var embed = $(embedId);
assertEqual(embed.lastError, lastError);
}
});
}