diff options
Diffstat (limited to 'dom/plugins/test/mochitest/test_object.html')
-rw-r--r-- | dom/plugins/test/mochitest/test_object.html | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/dom/plugins/test/mochitest/test_object.html b/dom/plugins/test/mochitest/test_object.html new file mode 100644 index 0000000000..093333ea5e --- /dev/null +++ b/dom/plugins/test/mochitest/test_object.html @@ -0,0 +1,510 @@ +<!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> |