<!DOCTYPE html> <html> <head> <title>Plugin instantiation</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script> <script type="application/javascript" src="plugin-utils.js"></script> <meta charset="utf-8"> <body onload="onLoad()"> <script class="testbody" type="text/javascript;version=1.8"> "use strict"; SimpleTest.waitForExplicitFinish(); setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED, "Test Plug-in"); // This can go away once embed also is on WebIDL let OBJLC = SpecialPowers.Ci.nsIObjectLoadingContent; // Use string modes in this test to make the test easier to read/debug. // nsIObjectLoadingContent refers to this as "type", but I am using "mode" // in the test to avoid confusing with content-type. let prettyModes = {}; prettyModes[OBJLC.TYPE_LOADING] = "loading"; prettyModes[OBJLC.TYPE_IMAGE] = "image"; prettyModes[OBJLC.TYPE_PLUGIN] = "plugin"; prettyModes[OBJLC.TYPE_DOCUMENT] = "document"; prettyModes[OBJLC.TYPE_NULL] = "none"; let body = document.body; // A single-pixel white png let testPNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3AoIFiETNqbNRQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAACklEQVQIHWP4DwABAQEANl9ngAAAAABJRU5ErkJggg=='; // An empty, but valid, SVG 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>'; // executeSoon wrapper to count pending callbacks let pendingCalls = 0; let afterPendingCalls = false; function runWhenDone(func) { if (!pendingCalls) func(); else afterPendingCalls = func; } function runSoon(func) { pendingCalls++; SimpleTest.executeSoon(function() { func(); if (--pendingCalls < 1 && afterPendingCalls) afterPendingCalls(); }); } function src(obj, state, uri) { // If we have a running plugin, src changing should always throw it out, // even if it causes us to load the same plugin again. if (uri && runningPlugin(obj, state)) { if (!state.oldPlugins) state.oldPlugins = []; try { state.oldPlugins.push(obj.getObjectValue()); } catch (e) { ok(false, "Running plugin but cannot call getObjectValue?"); } } var srcattr; if (state.tagName == "object") srcattr = "data"; else if (state.tagName == "embed") srcattr = "src"; else ok(false, "Internal test fail: Why are we setting the src of an applet"); // Plugins should always go away immediately on src/data change state.initialPlugin = false; if (uri === null) { removeAttr(obj, srcattr); // TODO Bug 767631 - we don't trigger loadObject on UnsetAttr :( forceReload(obj, state); } else { setAttr(obj, srcattr, uri); } } // We have to be careful not to reach past the nsObjectLoadingContent // prototype to touch generic element attributes, as this will try to // spawn the plugin, breaking our ability to test for that. function getAttr(obj, attr) { return document.body.constructor.prototype.getAttribute.call(obj, attr); } function setAttr(obj, attr, val) { return document.body.constructor.prototype.setAttribute.call(obj, attr, val); } function hasAttr(obj, attr) { return document.body.constructor.prototype.hasAttribute.call(obj, attr); } function removeAttr(obj, attr) { return document.body.constructor.prototype.removeAttribute.call(obj, attr); } function setDisplayed(obj, display) { if (display) removeAttr(obj, 'style'); else setAttr(obj, 'style', "display: none;"); } function displayed(obj) { // Hacky, but that's all we use style for. return !hasAttr(obj, 'style'); } function actualType(obj, state) { return state.getActualType.call(obj); } function getMode(obj, state) { return prettyModes[state.getDisplayedType.call(obj)]; } function runningPlugin(obj, state) { return state.getHasRunningPlugin.call(obj); } // TODO this is hacky and might hide some failures, but is needed until // Bug 767635 lands -- which itself will probably mean tweaking this test. function forceReload(obj, state) { let attr; if (state.tagName == "object") attr = "data"; else if (state.tagName == "embed") attr = "src"; if (attr && hasAttr(obj, attr)) { src(obj, state, getAttr(obj, attr)); } else if (body.contains(obj)) { body.appendChild(obj); } else { // Out of document nodes without data attributes simply can't be // reloaded currently. Bug 767635 } }; // Make a list of combinations of sub-lists, e.g.: // [ [a, b], [c, d] ] // -> // [ [a, c], [a, d], [b, c], [b, d] ] function eachList() { let all = []; if (!arguments.length) return all; let list = Array.prototype.slice.call(arguments, 0); for (let c of list[0]) { if (list.length > 1) { for (let x of eachList.apply(this,list.slice(1))) { all.push((c.length ? [c] : []).concat(x)); } } else if (c.length) { all.push([c]); } } return all; } let states = { svg: function(obj, state) { removeAttr(obj, "type"); src(obj, state, testSVG); state.noChannel = false; state.expectedType = "image/svg"; // SVGs are actually image-like subdocuments state.expectedMode = "document"; }, image: function(obj, state) { removeAttr(obj, "type"); src(obj, state, testPNG); state.noChannel = false; state.expectedMode = "image"; state.expectedType = "image/png"; }, plugin: function(obj, state) { removeAttr(obj, "type"); src(obj, state, "data:application/x-test,foo"); state.noChannel = false; state.expectedType = "application/x-test"; state.expectedMode = "plugin"; }, pluginExtension: function(obj, state) { src(obj, state, "./fake_plugin.tst"); state.expectedMode = "plugin"; state.pluginExtension = true; state.noChannel = false; }, document: function(obj, state) { removeAttr(obj, "type"); src(obj, state, "data:text/plain,I am a document"); state.noChannel = false; state.expectedType = "text/plain"; state.expectedMode = "document"; }, fallback: function(obj, state) { removeAttr(obj, "type"); state.expectedType = "application/x-unknown"; state.expectedMode = "none"; state.noChannel = true; src(obj, state, null); }, addToDoc: function(obj, state) { body.appendChild(obj); }, removeFromDoc: function(obj, state) { if (body.contains(obj)) body.removeChild(obj); }, // Set the proper type setType: function(obj, state) { if (state.expectedType) { state.badType = false; setAttr(obj, 'type', state.expectedType); forceReload(obj, state); } }, // Set an improper type setWrongType: function(obj, state) { // This should break no-channel-plugins but nothing else state.badType = true; setAttr(obj, 'type', "application/x-unknown"); forceReload(obj, state); }, // Set a plugin type setPluginType: function(obj, state) { // If an object/embed has a type set to a plugin type, it should not // use the channel type. state.badType = false; setAttr(obj, 'type', 'application/x-test'); state.expectedType = "application/x-test"; state.expectedMode = "plugin"; forceReload(obj, state); }, noChannel: function(obj, state) { src(obj, state, null); state.noChannel = true; state.pluginExtension = false; }, displayNone: function(obj, state) { setDisplayed(obj, false); }, displayInherit: function(obj, state) { setDisplayed(obj, true); } }; function testObject(obj, state) { // If our test combination both sets noChannel but no explicit type // it shouldn't load ever. let expectedMode = state.expectedMode; let actualMode = getMode(obj, state); if (state.noChannel && !getAttr(obj, 'type')) { // Some combinations of test both set no type and no channel. This is // worth testing with the various combinations, but shouldn't load. expectedMode = "none"; } // Embed tags should always try to load a plugin by type or extension // before falling back to opening a channel. See bug 803159 if (state.tagName == "embed" && (getAttr(obj, 'type') == "application/x-test" || state.pluginExtension)) { state.noChannel = true; } // with state.loading, unless we're loading with no channel, these types // should still be in loading state pending a channel. if (state.loading && (expectedMode == "image" || expectedMode == "document" || (expectedMode == "plugin" && !state.initialPlugin && !state.noChannel))) { expectedMode = "loading"; } // With the exception of plugins with a proper type, nothing should // load without a channel if (state.noChannel && (expectedMode != "plugin" || state.badType) && body.contains(obj)) { expectedMode = "none"; } // embed tags should reject documents, except for SVG images which // render as such if (state.tagName == "embed" && expectedMode == "document" && actualType(obj, state) != "image/svg+xml") { expectedMode = "none"; } // Embeds with a plugin type should skip opening a channel prior to // loading, taking only type into account. if (state.tagName == 'embed' && getAttr(obj, 'type') == 'application/x-test' && body.contains(obj)) { expectedMode = "plugin"; } if (!body.contains(obj) && (!state.loading || expectedMode != "image") && (!state.initialPlugin || expectedMode != "plugin")) { // Images are handled by nsIImageLoadingContent so we dont track // their state change as they're detached and reattached. All other // types switch to state "loading", and are completely unloaded expectedMode = "loading"; } is(actualMode, expectedMode, "check loaded mode"); // If we're a plugin, check that we spawned successfully. state.loading // is set if we haven't had an event loop since applying state, in which // case the plugin would not have stopped yet if it was initially a // plugin. let shouldBeSpawnable = expectedMode == "plugin" && displayed(obj); let shouldSpawn = shouldBeSpawnable && (!state.loading || state.initialPlugin); let didSpawn = runningPlugin(obj, state); is(didSpawn, !!shouldSpawn, "check plugin spawned is " + !!shouldSpawn); // If we are a plugin, scripting should work. If we're not spawned we // should spawn synchronously. let scripted = false; try { let x = obj.getObjectValue(); scripted = true; } catch(e) {} is(scripted, shouldBeSpawnable, "check plugin scriptability"); // If this tag previously had other spawned plugins, make sure it // respawned between then and now if (state.oldPlugins && didSpawn) { let didRespawn = false; for (let oldp of state.oldPlugins) { // If this returns false or throws, it's not the same plugin try { didRespawn = !obj.checkObjectValue(oldp); } catch (e) { didRespawn = true; } } is(didRespawn, true, "Plugin should have re-spawned since old state ("+state.oldPlugins.length+")"); } } let total = 0; let test_modes = { // Just apply from_state then to_state "immediate": function(obj, from_state, to_state, state) { for (let from of from_state) states[from](obj, state); for (let to of to_state) states[to](obj, state); // We don't spin the event loop between applying to_state and // running tests, so some types are still loading state.loading = true; info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / immediate"); testObject(obj, state); if (body.contains(obj)) body.removeChild(obj); }, // Apply states, spin event loop, run tests. "cycle": function(obj, from_state, to_state, state) { for (let from of from_state) states[from](obj, state); for (let to of to_state) states[to](obj, state); // Because re-appending to the document creates a script blocker, but // plugins spawn asynchronously, we need to return to the event loop // twice to ensure the plugin has been given a chance to lazily spawn. runSoon(function() { runSoon(function() { info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cycle"); testObject(obj, state); if (body.contains(obj)) body.removeChild(obj); }); }); }, // Apply initial state, spin event loop, apply final state, spin event // loop again. "cycleboth": function(obj, from_state, to_state, state) { for (let from of from_state) { states[from](obj, state); } runSoon(function() { for (let to of to_state) { states[to](obj, state); } // Because re-appending to the document creates a script blocker, // but plugins spawn asynchronously, we need to return to the event // loop twice to ensure the plugin has been given a chance to lazily // spawn. runSoon(function() { runSoon(function() { info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cycleboth"); testObject(obj, state); if (body.contains(obj)) body.removeChild(obj); }); }); }); }, // Apply initial state, spin event loop, apply later state, test // immediately "cyclefirst": function(obj, from_state, to_state, state) { for (let from of from_state) { states[from](obj, state); } runSoon(function() { state.initialPlugin = runningPlugin(obj, state); for (let to of to_state) { states[to](obj, state); } info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cyclefirst"); // We don't spin the event loop between applying to_state and // running tests, so some types are still loading state.loading = true; testObject(obj, state); if (body.contains(obj)) body.removeChild(obj); }); }, }; function test(testdat) { // FIXME bug 1291854: Change back to lets when the test is fixed. for (var from_state of testdat['from_states']) { for (var to_state of testdat['to_states']) { for (var mode of testdat['test_modes']) { for (var type of testdat['tag_types']) { runSoon(function () { let obj = document.createElement(type); obj.width = 1; obj.height = 1; let state = {}; state.noChannel = true; state.tagName = type; // Part of the test checks whether a plugin spawned or not, // but touching the object prototype will attempt to // synchronously spawn a plugin! We use this terrible hack to // get a privileged getter for the attributes we want to touch // prior to applying any attributes. // TODO when embed goes away we wont need to check for // QueryInterface any longer. var lookup_on = obj.QueryInterface ? obj.QueryInterface(OBJLC): obj; state.getDisplayedType = SpecialPowers.do_lookupGetter(lookup_on, 'displayedType'); state.getHasRunningPlugin = SpecialPowers.do_lookupGetter(lookup_on, 'hasRunningPlugin'); state.getActualType = SpecialPowers.do_lookupGetter(lookup_on, 'actualType'); test_modes[mode](obj, from_state, to_state, state); }); } } } } } function onLoad() { // Generic tests test({ 'tag_types': [ 'embed', 'object' ], // In all three modes 'test_modes': [ 'immediate', 'cycle', 'cyclefirst', 'cycleboth' ], // Starting from a blank tag in and out of the document, a loading // plugin, and no-channel plugin (initial types only really have // odd cases with plugins) 'from_states': [ [ 'addToDoc' ], [ 'plugin' ], [ 'plugin', 'addToDoc' ], [ 'plugin', 'noChannel', 'setType', 'addToDoc' ], [], ], // To various combinations of loaded objects 'to_states': eachList( [ 'svg', 'image', 'plugin', 'document', '' ], [ 'setType', 'setWrongType', 'setPluginType', '' ], [ 'noChannel', '' ], [ 'displayNone', 'displayInherit', '' ] )}); // Special case test for embed tags with plugin-by-extension // TODO object tags should be tested here too -- they have slightly // different behavior, but waiting on a file load requires a loaded // event handler and wont work with just our event loop spinning. test({ 'tag_types': [ 'embed' ], 'test_modes': [ 'immediate', 'cyclefirst', 'cycle', 'cycleboth' ], 'from_states': eachList( [ 'svg', 'plugin', 'image', 'document' ], [ 'addToDoc' ] ), // Set extension along with valid ty 'to_states': [ [ 'pluginExtension' ] ]}); // Test plugin add/remove from document with adding/removing frame, with // and without a channel. test({ 'tag_types': [ 'embed', 'object' ], // Ideally we'd test object too, but this gets exponentially long. 'test_modes': [ 'immediate', 'cyclefirst', 'cycle' ], 'from_states': [ [ 'displayNone', 'plugin', 'addToDoc' ], [ 'displayNone', 'plugin', 'noChannel', 'addToDoc' ], [ 'plugin', 'noChannel', 'addToDoc' ], [ 'plugin', 'noChannel' ] ], 'to_states': eachList( [ 'displayNone', '' ], [ 'removeFromDoc' ], [ 'image', 'displayNone', '' ], [ 'image', 'displayNone', '' ], [ 'addToDoc' ], [ 'displayInherit' ] )}); runWhenDone(() => SimpleTest.finish()); } </script>