1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/content/base/test/test_object.html Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,515 @@ 1.4 +<!DOCTYPE html> 1.5 +<html> 1.6 + <head> 1.7 + <title>Plugin instantiation</title> 1.8 + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> 1.9 + <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script> 1.10 + <meta charset="utf-8"> 1.11 + <body onload="onLoad()"> 1.12 + <script class="testbody" type="text/javascript;version=1.8"> 1.13 + 1.14 + "use strict"; 1.15 + SimpleTest.waitForExplicitFinish(); 1.16 + 1.17 + var pluginHost = SpecialPowers.Cc["@mozilla.org/plugin/host;1"] 1.18 + .getService(SpecialPowers.Ci.nsIPluginHost); 1.19 + var pluginTags = pluginHost.getPluginTags(); 1.20 + for (var tag of pluginTags) { 1.21 + if (tag.name == "Test Plug-in") { 1.22 + tag.enabledState = SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED;; 1.23 + } 1.24 + } 1.25 + 1.26 + // This can go away once embed also is on WebIDL 1.27 + let OBJLC = SpecialPowers.Ci.nsIObjectLoadingContent; 1.28 + 1.29 + // Use string modes in this test to make the test easier to read/debug. 1.30 + // nsIObjectLoadingContent refers to this as "type", but I am using "mode" 1.31 + // in the test to avoid confusing with content-type. 1.32 + let prettyModes = {}; 1.33 + prettyModes[OBJLC.TYPE_LOADING] = "loading"; 1.34 + prettyModes[OBJLC.TYPE_IMAGE] = "image"; 1.35 + prettyModes[OBJLC.TYPE_PLUGIN] = "plugin"; 1.36 + prettyModes[OBJLC.TYPE_DOCUMENT] = "document"; 1.37 + prettyModes[OBJLC.TYPE_NULL] = "none"; 1.38 + 1.39 + let body = document.body; 1.40 + // A single-pixel white png 1.41 + let testPNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3AoIFiETNqbNRQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAACklEQVQIHWP4DwABAQEANl9ngAAAAABJRU5ErkJggg=='; 1.42 + // An empty, but valid, SVG 1.43 + let testSVG = 'data:image/svg+xml,<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'; 1.44 + // executeSoon wrapper to count pending callbacks 1.45 + let pendingCalls = 0; 1.46 + let afterPendingCalls = false; 1.47 + 1.48 + function runWhenDone(func) { 1.49 + if (!pendingCalls) 1.50 + func(); 1.51 + else 1.52 + afterPendingCalls = func; 1.53 + } 1.54 + function runSoon(func) { 1.55 + pendingCalls++; 1.56 + SimpleTest.executeSoon(function() { 1.57 + func(); 1.58 + if (--pendingCalls < 1 && afterPendingCalls) 1.59 + afterPendingCalls(); 1.60 + }); 1.61 + } 1.62 + function src(obj, state, uri) { 1.63 + // If we have a running plugin, src changing should always throw it out, 1.64 + // even if it causes us to load the same plugin again. 1.65 + if (uri && runningPlugin(obj, state)) { 1.66 + if (!state.oldPlugins) 1.67 + state.oldPlugins = []; 1.68 + try { 1.69 + state.oldPlugins.push(obj.getObjectValue()); 1.70 + } catch (e) { 1.71 + ok(false, "Running plugin but cannot call getObjectValue?"); 1.72 + } 1.73 + } 1.74 + 1.75 + var srcattr; 1.76 + if (state.tagName == "object") 1.77 + srcattr = "data"; 1.78 + else if (state.tagName == "embed") 1.79 + srcattr = "src"; 1.80 + else 1.81 + ok(false, "Internal test fail: Why are we setting the src of an applet"); 1.82 + 1.83 + // Plugins should always go away immediately on src/data change 1.84 + state.initialPlugin = false; 1.85 + if (uri === null) { 1.86 + removeAttr(obj, srcattr); 1.87 + // TODO Bug 767631 - we don't trigger loadObject on UnsetAttr :( 1.88 + forceReload(obj, state); 1.89 + } else { 1.90 + setAttr(obj, srcattr, uri); 1.91 + } 1.92 + } 1.93 + // We have to be careful not to reach past the nsObjectLoadingContent 1.94 + // prototype to touch generic element attributes, as this will try to 1.95 + // spawn the plugin, breaking our ability to test for that. 1.96 + function getAttr(obj, attr) { 1.97 + return document.body.constructor.prototype.getAttribute.call(obj, attr); 1.98 + } 1.99 + function setAttr(obj, attr, val) { 1.100 + return document.body.constructor.prototype.setAttribute.call(obj, attr, val); 1.101 + } 1.102 + function hasAttr(obj, attr) { 1.103 + return document.body.constructor.prototype.hasAttribute.call(obj, attr); 1.104 + } 1.105 + function removeAttr(obj, attr) { 1.106 + return document.body.constructor.prototype.removeAttribute.call(obj, attr); 1.107 + } 1.108 + function setDisplayed(obj, display) { 1.109 + if (display) 1.110 + removeAttr(obj, 'style'); 1.111 + else 1.112 + setAttr(obj, 'style', "display: none;"); 1.113 + } 1.114 + function displayed(obj) { 1.115 + // Hacky, but that's all we use style for. 1.116 + return !hasAttr(obj, 'style'); 1.117 + } 1.118 + function actualType(obj, state) { 1.119 + return state.getActualType.call(obj); 1.120 + } 1.121 + function getMode(obj, state) { 1.122 + return prettyModes[state.getDisplayedType.call(obj)]; 1.123 + } 1.124 + function runningPlugin(obj, state) { 1.125 + return state.getHasRunningPlugin.call(obj); 1.126 + } 1.127 + 1.128 + // TODO this is hacky and might hide some failures, but is needed until 1.129 + // Bug 767635 lands -- which itself will probably mean tweaking this test. 1.130 + function forceReload(obj, state) { 1.131 + let attr; 1.132 + if (state.tagName == "object") 1.133 + attr = "data"; 1.134 + else if (state.tagName == "embed") 1.135 + attr = "src"; 1.136 + 1.137 + if (attr && hasAttr(obj, attr)) { 1.138 + src(obj, state, getAttr(obj, attr)); 1.139 + } else if (body.contains(obj)) { 1.140 + body.appendChild(obj); 1.141 + } else { 1.142 + // Out of document nodes without data attributes simply can't be 1.143 + // reloaded currently. Bug 767635 1.144 + } 1.145 + }; 1.146 + 1.147 + // Make a list of combinations of sub-lists, e.g.: 1.148 + // [ [a, b], [c, d] ] 1.149 + // -> 1.150 + // [ [a, c], [a, d], [b, c], [b, d] ] 1.151 + function eachList() { 1.152 + let all = []; 1.153 + if (!arguments.length) 1.154 + return all; 1.155 + let list = Array.prototype.slice.call(arguments, 0); 1.156 + for (let c of list[0]) { 1.157 + if (list.length > 1) { 1.158 + for (let x of eachList.apply(this,list.slice(1))) { 1.159 + all.push((c.length ? [c] : []).concat(x)); 1.160 + } 1.161 + } else if (c.length) { 1.162 + all.push([c]); 1.163 + } 1.164 + } 1.165 + return all; 1.166 + } 1.167 + 1.168 + let states = { 1.169 + svg: function(obj, state) { 1.170 + removeAttr(obj, "type"); 1.171 + src(obj, state, testSVG); 1.172 + state.noChannel = false; 1.173 + state.expectedType = "image/svg"; 1.174 + // SVGs are actually image-like subdocuments 1.175 + state.expectedMode = "document"; 1.176 + }, 1.177 + image: function(obj, state) { 1.178 + removeAttr(obj, "type"); 1.179 + src(obj, state, testPNG); 1.180 + state.noChannel = false; 1.181 + state.expectedMode = "image"; 1.182 + state.expectedType = "image/png"; 1.183 + }, 1.184 + plugin: function(obj, state) { 1.185 + removeAttr(obj, "type"); 1.186 + src(obj, state, "data:application/x-test,foo"); 1.187 + state.noChannel = false; 1.188 + state.expectedType = "application/x-test"; 1.189 + state.expectedMode = "plugin"; 1.190 + }, 1.191 + pluginExtension: function(obj, state) { 1.192 + src(obj, state, "./fake_plugin.tst"); 1.193 + state.expectedMode = "plugin"; 1.194 + state.pluginExtension = true; 1.195 + state.noChannel = false; 1.196 + }, 1.197 + document: function(obj, state) { 1.198 + removeAttr(obj, "type"); 1.199 + src(obj, state, "data:text/plain,I am a document"); 1.200 + state.noChannel = false; 1.201 + state.expectedType = "text/plain"; 1.202 + state.expectedMode = "document"; 1.203 + }, 1.204 + fallback: function(obj, state) { 1.205 + removeAttr(obj, "type"); 1.206 + state.expectedType = "application/x-unknown"; 1.207 + state.expectedMode = "none"; 1.208 + state.noChannel = true; 1.209 + src(obj, state, null); 1.210 + }, 1.211 + addToDoc: function(obj, state) { 1.212 + body.appendChild(obj); 1.213 + }, 1.214 + removeFromDoc: function(obj, state) { 1.215 + if (body.contains(obj)) 1.216 + body.removeChild(obj); 1.217 + }, 1.218 + // Set the proper type 1.219 + setType: function(obj, state) { 1.220 + if (state.expectedType) { 1.221 + state.badType = false; 1.222 + setAttr(obj, 'type', state.expectedType); 1.223 + forceReload(obj, state); 1.224 + } 1.225 + }, 1.226 + // Set an improper type 1.227 + setWrongType: function(obj, state) { 1.228 + // This should break no-channel-plugins but nothing else 1.229 + state.badType = true; 1.230 + setAttr(obj, 'type', "application/x-unknown"); 1.231 + forceReload(obj, state); 1.232 + }, 1.233 + // Set a plugin type 1.234 + setPluginType: function(obj, state) { 1.235 + // If an object/embed has a type set to a plugin type, it should not 1.236 + // use the channel type. 1.237 + state.badType = false; 1.238 + setAttr(obj, 'type', 'application/x-test'); 1.239 + state.expectedType = "application/x-test"; 1.240 + state.expectedMode = "plugin"; 1.241 + forceReload(obj, state); 1.242 + }, 1.243 + noChannel: function(obj, state) { 1.244 + src(obj, state, null); 1.245 + state.noChannel = true; 1.246 + state.pluginExtension = false; 1.247 + }, 1.248 + displayNone: function(obj, state) { 1.249 + setDisplayed(obj, false); 1.250 + }, 1.251 + displayInherit: function(obj, state) { 1.252 + setDisplayed(obj, true); 1.253 + } 1.254 + }; 1.255 + 1.256 + 1.257 + function testObject(obj, state) { 1.258 + // If our test combination both sets noChannel but no explicit type 1.259 + // it shouldn't load ever. 1.260 + let expectedMode = state.expectedMode; 1.261 + let actualMode = getMode(obj, state); 1.262 + 1.263 + if (state.noChannel && !getAttr(obj, 'type')) { 1.264 + // Some combinations of test both set no type and no channel. This is 1.265 + // worth testing with the various combinations, but shouldn't load. 1.266 + expectedMode = "none"; 1.267 + } 1.268 + 1.269 + // Embed tags should always try to load a plugin by type or extension 1.270 + // before falling back to opening a channel. See bug 803159 1.271 + if (state.tagName == "embed" && 1.272 + (getAttr(obj, 'type') == "application/x-test" || state.pluginExtension)) { 1.273 + state.noChannel = true; 1.274 + } 1.275 + 1.276 + // with state.loading, unless we're loading with no channel, these types 1.277 + // should still be in loading state pending a channel. 1.278 + if (state.loading && (expectedMode == "image" || expectedMode == "document" || 1.279 + (expectedMode == "plugin" && !state.initialPlugin && !state.noChannel))) { 1.280 + expectedMode = "loading"; 1.281 + } 1.282 + 1.283 + // With the exception of plugins with a proper type, nothing should 1.284 + // load without a channel 1.285 + if (state.noChannel && (expectedMode != "plugin" || state.badType) && 1.286 + body.contains(obj)) { 1.287 + expectedMode = "none"; 1.288 + } 1.289 + 1.290 + // embed tags should reject documents, except for SVG images which 1.291 + // render as such 1.292 + if (state.tagName == "embed" && expectedMode == "document" && 1.293 + actualType(obj, state) != "image/svg+xml") { 1.294 + expectedMode = "none"; 1.295 + } 1.296 + 1.297 + // Embeds with a plugin type should skip opening a channel prior to 1.298 + // loading, taking only type into account. 1.299 + if (state.tagName == 'embed' && getAttr(obj, 'type') == 'application/x-test' && 1.300 + body.contains(obj)) { 1.301 + expectedMode = "plugin"; 1.302 + } 1.303 + 1.304 + if (!body.contains(obj) 1.305 + && (!state.loading || expectedMode != "image") 1.306 + && (!state.initialPlugin || expectedMode != "plugin")) { 1.307 + // Images are handled by nsIImageLoadingContent so we dont track 1.308 + // their state change as they're detached and reattached. All other 1.309 + // types switch to state "loading", and are completely unloaded 1.310 + expectedMode = "loading"; 1.311 + } 1.312 + 1.313 + is(actualMode, expectedMode, "check loaded mode"); 1.314 + 1.315 + // If we're a plugin, check that we spawned successfully. state.loading 1.316 + // is set if we haven't had an event loop since applying state, in which 1.317 + // case the plugin would not have stopped yet if it was initially a 1.318 + // plugin. 1.319 + let shouldBeSpawnable = expectedMode == "plugin" && displayed(obj); 1.320 + let shouldSpawn = shouldBeSpawnable && (!state.loading || state.initialPlugin); 1.321 + let didSpawn = runningPlugin(obj, state); 1.322 + is(didSpawn, !!shouldSpawn, "check plugin spawned is " + !!shouldSpawn); 1.323 + 1.324 + // If we are a plugin, scripting should work. If we're not spawned we 1.325 + // should spawn synchronously. 1.326 + let scripted = false; 1.327 + try { 1.328 + let x = obj.getObjectValue(); 1.329 + scripted = true; 1.330 + } catch(e) {} 1.331 + is(scripted, shouldBeSpawnable, "check plugin scriptability"); 1.332 + 1.333 + // If this tag previously had other spawned plugins, make sure it 1.334 + // respawned between then and now 1.335 + if (state.oldPlugins && didSpawn) { 1.336 + let didRespawn = false; 1.337 + for (let oldp of state.oldPlugins) { 1.338 + // If this returns false or throws, it's not the same plugin 1.339 + try { 1.340 + didRespawn = !obj.checkObjectValue(oldp); 1.341 + } catch (e) { 1.342 + didRespawn = true; 1.343 + } 1.344 + } 1.345 + is(didRespawn, true, "Plugin should have re-spawned since old state ("+state.oldPlugins.length+")"); 1.346 + } 1.347 + } 1.348 + 1.349 + let total = 0; 1.350 + let test_modes = { 1.351 + // Just apply from_state then to_state 1.352 + "immediate": function(obj, from_state, to_state, state) { 1.353 + for (let from of from_state) 1.354 + states[from](obj, state); 1.355 + for (let to of to_state) 1.356 + states[to](obj, state); 1.357 + 1.358 + // We don't spin the event loop between applying to_state and 1.359 + // running tests, so some types are still loading 1.360 + state.loading = true; 1.361 + info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / immediate"); 1.362 + testObject(obj, state); 1.363 + 1.364 + if (body.contains(obj)) 1.365 + body.removeChild(obj); 1.366 + 1.367 + }, 1.368 + // Apply states, spin event loop, run tests. 1.369 + "cycle": function(obj, from_state, to_state, state) { 1.370 + for (let from of from_state) 1.371 + states[from](obj, state); 1.372 + for (let to of to_state) 1.373 + states[to](obj, state); 1.374 + // Because re-appending to the document creates a script blocker, but 1.375 + // plugins spawn asynchronously, we need to return to the event loop 1.376 + // twice to ensure the plugin has been given a chance to lazily spawn. 1.377 + runSoon(function() { runSoon(function() { 1.378 + info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cycle"); 1.379 + testObject(obj, state); 1.380 + 1.381 + if (body.contains(obj)) 1.382 + body.removeChild(obj); 1.383 + }); }); 1.384 + }, 1.385 + // Apply initial state, spin event loop, apply final state, spin event 1.386 + // loop again. 1.387 + "cycleboth": function(obj, from_state, to_state, state) { 1.388 + for (let from of from_state) { 1.389 + states[from](obj, state); 1.390 + } 1.391 + runSoon(function() { 1.392 + for (let to of to_state) { 1.393 + states[to](obj, state); 1.394 + } 1.395 + // Because re-appending to the document creates a script blocker, 1.396 + // but plugins spawn asynchronously, we need to return to the event 1.397 + // loop twice to ensure the plugin has been given a chance to lazily 1.398 + // spawn. 1.399 + runSoon(function() { runSoon(function() { 1.400 + info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cycleboth"); 1.401 + testObject(obj, state); 1.402 + 1.403 + if (body.contains(obj)) 1.404 + body.removeChild(obj); 1.405 + }); }); 1.406 + }); 1.407 + }, 1.408 + // Apply initial state, spin event loop, apply later state, test 1.409 + // immediately 1.410 + "cyclefirst": function(obj, from_state, to_state, state) { 1.411 + for (let from of from_state) { 1.412 + states[from](obj, state); 1.413 + } 1.414 + runSoon(function() { 1.415 + state.initialPlugin = runningPlugin(obj, state); 1.416 + for (let to of to_state) { 1.417 + states[to](obj, state); 1.418 + } 1.419 + info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cyclefirst"); 1.420 + // We don't spin the event loop between applying to_state and 1.421 + // running tests, so some types are still loading 1.422 + state.loading = true; 1.423 + testObject(obj, state); 1.424 + 1.425 + if (body.contains(obj)) 1.426 + body.removeChild(obj); 1.427 + }); 1.428 + }, 1.429 + }; 1.430 + 1.431 + function test(testdat) { 1.432 + for (let from_state of testdat['from_states']) { 1.433 + for (let to_state of testdat['to_states']) { 1.434 + for (let mode of testdat['test_modes']) { 1.435 + for (let type of testdat['tag_types']) { 1.436 + runSoon(function () { 1.437 + let obj = document.createElement(type); 1.438 + obj.width = 1; obj.height = 1; 1.439 + let state = {}; 1.440 + state.noChannel = true; 1.441 + state.tagName = type; 1.442 + // Part of the test checks whether a plugin spawned or not, 1.443 + // but touching the object prototype will attempt to 1.444 + // synchronously spawn a plugin! We use this terrible hack to 1.445 + // get a privileged getter for the attributes we want to touch 1.446 + // prior to applying any attributes. 1.447 + // TODO when embed goes away we wont need to check for 1.448 + // QueryInterface any longer. 1.449 + var lookup_on = obj.QueryInterface ? obj.QueryInterface(OBJLC): obj; 1.450 + state.getDisplayedType = SpecialPowers.do_lookupGetter(lookup_on, 'displayedType'); 1.451 + state.getHasRunningPlugin = SpecialPowers.do_lookupGetter(lookup_on, 'hasRunningPlugin'); 1.452 + state.getActualType = SpecialPowers.do_lookupGetter(lookup_on, 'actualType'); 1.453 + test_modes[mode](obj, from_state, to_state, state); 1.454 + }); 1.455 + } 1.456 + } 1.457 + } 1.458 + } 1.459 + } 1.460 + 1.461 + function onLoad() { 1.462 + // Generic tests 1.463 + test({ 1.464 + 'tag_types': [ 'embed', 'object' ], 1.465 + // In all three modes 1.466 + 'test_modes': [ 'immediate', 'cycle', 'cyclefirst', 'cycleboth' ], 1.467 + // Starting from a blank tag in and out of the document, a loading 1.468 + // plugin, and no-channel plugin (initial types only really have 1.469 + // odd cases with plugins) 1.470 + 'from_states': [ 1.471 + [ 'addToDoc' ], 1.472 + [ 'plugin' ], 1.473 + [ 'plugin', 'addToDoc' ], 1.474 + [ 'plugin', 'noChannel', 'setType', 'addToDoc' ], 1.475 + [], 1.476 + ], 1.477 + // To various combinations of loaded objects 1.478 + 'to_states': eachList( 1.479 + [ 'svg', 'image', 'plugin', 'document', '' ], 1.480 + [ 'setType', 'setWrongType', 'setPluginType', '' ], 1.481 + [ 'noChannel', '' ], 1.482 + [ 'displayNone', 'displayInherit', '' ] 1.483 + )}); 1.484 + // Special case test for embed tags with plugin-by-extension 1.485 + // TODO object tags should be tested here too -- they have slightly 1.486 + // different behavior, but waiting on a file load requires a loaded 1.487 + // event handler and wont work with just our event loop spinning. 1.488 + test({ 1.489 + 'tag_types': [ 'embed' ], 1.490 + 'test_modes': [ 'immediate', 'cyclefirst', 'cycle', 'cycleboth' ], 1.491 + 'from_states': eachList( 1.492 + [ 'svg', 'plugin', 'image', 'document' ], 1.493 + [ 'addToDoc' ] 1.494 + ), 1.495 + // Set extension along with valid ty 1.496 + 'to_states': [ 1.497 + [ 'pluginExtension' ] 1.498 + ]}); 1.499 + // Test plugin add/remove from document with adding/removing frame, with 1.500 + // and without a channel. 1.501 + test({ 1.502 + 'tag_types': [ 'embed', 'object' ], // Ideally we'd test object too, but this gets exponentially long. 1.503 + 'test_modes': [ 'immediate', 'cyclefirst', 'cycle' ], 1.504 + 'from_states': [ [ 'displayNone', 'plugin', 'addToDoc' ], 1.505 + [ 'displayNone', 'plugin', 'noChannel', 'addToDoc' ], 1.506 + [ 'plugin', 'noChannel', 'addToDoc' ], 1.507 + [ 'plugin', 'noChannel' ] ], 1.508 + 'to_states': eachList( 1.509 + [ 'displayNone', '' ], 1.510 + [ 'removeFromDoc' ], 1.511 + [ 'image', 'displayNone', '' ], 1.512 + [ 'image', 'displayNone', '' ], 1.513 + [ 'addToDoc' ], 1.514 + [ 'displayInherit' ] 1.515 + )}); 1.516 + runWhenDone(function() SimpleTest.finish()); 1.517 + } 1.518 + </script>