1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Class to track the progress events received by a particular plugin instance. 6 function EventStateMachine() { 7 // Work around how JS binds 'this'. 8 var this_ = this; 9 // Given a particular state, what are the acceptable event types. 10 this.expectedNext = { 11 'BEGIN': { 'loadstart': 1 }, 12 'loadstart': { 'progress': 1, 'error': 1, 'abort': 1, 'load': 1 }, 13 'progress': { 'progress': 1, 'error': 1, 'abort': 1, 'load': 1 }, 14 'error': { 'loadend': 1 }, 15 'abort': { 'loadend': 1 }, 16 'load': { 'loadend': 1 }, 17 'loadend': { }, 18 'UNEXPECTED': { }, 19 }; 20 // The current state (and index into expectedNext). 21 this.currentState = 'BEGIN'; 22 // For each recognized state, a count of the times it was reached. 23 this.stateHistogram = { 24 'BEGIN': 0, 25 'loadstart': 0, 26 'progress': 0, 27 'error': 0, 28 'abort': 0, 29 'load': 0, 30 'loadend': 0, 31 'UNEXPECTED': 0 32 }; 33 // The state transition function. 34 this.transitionTo = function(event_type) { 35 // The index values of this_.expectedNext are the only valid states. 36 // Invalid event types are normalized to 'UNEXPECTED'. 37 if (this_.expectedNext[event_type] == undefined) { 38 console.log('unexpected ' + event_type); 39 event_type = 'UNEXPECTED'; 40 } 41 // Check that the next event type is expected from the current state. 42 // If not, we transition to the state 'UNEXPECTED'. 43 if (!(event_type in this_.expectedNext[this_.currentState])) { 44 console.log('unexpected ' + event_type + ' from ' + this_.currentState); 45 event_type = 'UNEXPECTED'; 46 } 47 this_.currentState = event_type; 48 this_.stateHistogram[this_.currentState]++; 49 } 50 51 // True if an event with lengthComputable is ever triggered. 52 this.stateSawLengthComputable = false; 53 // The last event.total seen from an event with lengthComputable being true. 54 this.stateProgressTotal = -1; 55 // The last event.loaded seen from an event with lengthComputable being true. 56 this.stateProgressPrev = -1; 57 // Function to record progress stats. 58 this.recordProgress = function(event) { 59 // Can either record progress from a progress event with lengthComputable, 60 // or from a loadend event. 61 if (event.type == 'progress' && event.lengthComputable) { 62 this.stateSawLengthComputable = true; 63 this.stateProgressTotal = event.total; 64 this.stateProgressPrev = event.loaded; 65 } else if (event.type == 'loadend' && event.lengthComputable) { 66 this.stateProgressTotal = event.total; 67 this.stateProgressPrev = event.loaded; 68 } 69 } 70 } 71 72 // event_machines is a collection of EventStateMachines, one for each element 73 // id that dispatches an event of a type we are listening for. 74 window.event_machines = { }; 75 // Look up the EventStateMachine for the id. 76 function lookupEventMachine(element_id) { 77 var event_machine = window.event_machines[element_id]; 78 if (event_machine == undefined) { 79 // This is the first event for this target. Create an EventStateMachine. 80 event_machine = new EventStateMachine(); 81 window.event_machines[element_id] = event_machine; 82 } 83 return event_machine; 84 } 85 // Sets up event listeners on the body element for all the progress 86 // event types. Delegation to the body allows this to be done only once 87 // per document. 88 var setListeners = function(body_element) { 89 var eventListener = function(e) { 90 // Find the target element of the event. 91 var target_element = e.target; 92 // Body only dispatches for elements having the 'naclModule' CSS class. 93 if (target_element.className != 'naclModule') { 94 return; 95 } 96 var element_id = target_element.id; 97 // Look up the EventStateMachine for the target of the event. 98 var event_machine = lookupEventMachine(element_id); 99 // Update the state of the machine. 100 event_machine.transitionTo(e.type); 101 // Record progress information if possible. 102 event_machine.recordProgress(e); 103 } 104 // Add the listener for all of the ProgressEvent event types. 105 body_element.addEventListener('loadstart', eventListener, true); 106 body_element.addEventListener('progress', eventListener, true); 107 body_element.addEventListener('error', eventListener, true); 108 body_element.addEventListener('abort', eventListener, true); 109 body_element.addEventListener('load', eventListener, true); 110 body_element.addEventListener('loadend', eventListener, true); 111 } 112 113 // Performs some tests to make sure that progress events follow the expected 114 // state transitions to end in an expected state. 115 function testProgressEventStateMachine(tester, 116 embedId, 117 progressMinCount, 118 errorCount, 119 abortCount, 120 loadCount, 121 lastError) { 122 var eventMachine = lookupEventMachine(embedId); 123 // Test the expected number of occurrences, with some duplication. 124 tester.addTest('begin_count_' + embedId, function() { 125 // There should be no 'BEGIN' event. 126 assertEqual(eventMachine.stateHistogram['BEGIN'], 0); 127 }); 128 tester.addTest('loadstart_count_' + embedId, function() { 129 // There should be one 'loadstart' event. 130 assertEqual(eventMachine.stateHistogram['loadstart'], 1); 131 }); 132 tester.addTest('progress_min_count_' + embedId, function() { 133 // There should be at least one progress event when the manifest file is 134 // loaded and another when the .nexe is loaded. 135 assert(eventMachine.stateHistogram['progress'] >= progressMinCount); 136 }); 137 tester.addTest('progress_samples_' + embedId, function() { 138 console.log('stateSawLengthComputable ' + 139 eventMachine.stateSawLengthComputable); 140 console.log('stateProgressPrev ' + 141 eventMachine.stateProgressPrev); 142 console.log('stateProgressTotal ' + 143 eventMachine.stateProgressTotal); 144 145 assert(eventMachine.stateSawLengthComputable); 146 // Progress events are not necessarily monotonic. For glibc, each DSO 147 // will trigger a different series of progress events with different totals. 148 // For glibc, the final loadend progress event may even correspond to 149 // the very first load event, instead of corresponding to the last... 150 // So, all we check is that the latest values make some sense. 151 assert(eventMachine.stateProgressPrev > 0); 152 assert(eventMachine.stateProgressTotal > 0); 153 assert(eventMachine.stateProgressPrev <= eventMachine.stateProgressTotal); 154 }); 155 tester.addTest('error_count_' + embedId, function() { 156 // Check that the right number of 'error' events were dispatched. 157 assertEqual(eventMachine.stateHistogram['error'], errorCount); 158 }); 159 tester.addTest('abort_count_' + embedId, function() { 160 // Check that the right number of 'abort' events were dispatched. 161 assertEqual(eventMachine.stateHistogram['abort'], abortCount); 162 }); 163 tester.addTest('load_count_' + embedId, function() { 164 // Check that the right number of 'load' events were dispatched. 165 assertEqual(eventMachine.stateHistogram['load'], loadCount); 166 }) 167 tester.addTest('loadend_count_' + embedId, function() { 168 // There should be one 'loadend' event. 169 assertEqual(eventMachine.stateHistogram['loadend'], 1); 170 }); 171 tester.addTest('unexpected_count_' + embedId, function() { 172 // There should be no 'UNEXPECTED' event. 173 assertEqual(eventMachine.stateHistogram['UNEXPECTED'], 0); 174 }); 175 tester.addTest('end_state_' + embedId, function() { 176 // Test that the progress events followed the expected sequence to 177 // completion in the 'loadend' state. 178 assertEqual(eventMachine.currentState, 'loadend'); 179 }); 180 tester.addTest('last_error_string_' + embedId, function() { 181 // If an error or abort was reported, check that lastError is set 182 // to the correct value. 183 if ((eventMachine.stateHistogram['error'] > 0 || 184 eventMachine.stateHistogram['abort'] > 0)) { 185 var embed = $(embedId); 186 assertEqual(embed.lastError, lastError); 187 } 188 }); 189 } 190