Bug 783059 - Plugin instantiation tests for embed/object tags. r=josh

This commit is contained in:
John Schoenick 2013-03-07 16:11:27 -08:00
parent a878fe7370
commit 7216504a17
7 changed files with 534 additions and 2 deletions

View File

@ -25,7 +25,7 @@ interface nsIURI;
* interface to mirror this interface when changing it.
*/
[scriptable, uuid(e9102696-4bb4-4a98-a31f-be9da5a3d18e)]
[scriptable, uuid(b6c5ae8a-5e30-41f1-975a-be73cd6f71db)]
interface nsIObjectLoadingContent : nsISupports
{
/**
@ -165,6 +165,12 @@ interface nsIObjectLoadingContent : nsISupports
readonly attribute unsigned long pluginFallbackType;
/**
* If this object currently owns a running plugin, regardless of whether or
* not one is pending spawn/despawn.
*/
readonly attribute bool hasRunningPlugin;
/**
* This method will disable the play-preview plugin state.
*/

View File

@ -2645,6 +2645,14 @@ nsObjectLoadingContent::GetPluginFallbackType(uint32_t* aPluginFallbackType)
return NS_OK;
}
NS_IMETHODIMP
nsObjectLoadingContent::GetHasRunningPlugin(bool *aHasPlugin)
{
NS_ENSURE_TRUE(nsContentUtils::IsCallerChrome(), NS_ERROR_NOT_AVAILABLE);
*aHasPlugin = HasRunningPlugin();
return NS_OK;
}
NS_IMETHODIMP
nsObjectLoadingContent::CancelPlayPreview()
{
@ -2652,7 +2660,7 @@ nsObjectLoadingContent::CancelPlayPreview()
return NS_ERROR_NOT_AVAILABLE;
mPlayPreviewCanceled = true;
// If we're in play preview state already, reload
if (mType == eType_Null && mFallbackType == eFallbackPlayPreview) {
return LoadObject(true, true);

View File

@ -197,6 +197,10 @@ class nsObjectLoadingContent : public nsImageLoadingContent
{
return mFallbackType;
}
bool HasRunningPlugin() const
{
return !!mInstanceOwner;
}
void CancelPlayPreview(mozilla::ErrorResult& aRv)
{
aRv = CancelPlayPreview();

View File

@ -618,6 +618,7 @@ MOCHITEST_FILES_C= \
file_bothCSPheaders.html^headers^ \
badMessageEvent2.eventsource \
badMessageEvent2.eventsource^headers^ \
test_object.html \
$(NULL)
# OOP tests don't work on Windows (bug 763081) or native-fennec

View File

@ -0,0 +1 @@
This is used in test_object.html to test loading by extension (.tst -> application/x-test).

View File

@ -0,0 +1,505 @@
<!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>
<meta charset="utf-8">
<body onload="onLoad()">
<script class="testbody" type="text/javascript;version=1.8">
"use strict";
SimpleTest.waitForExplicitFinish();
// This can go away once embed also is on WebIDL
let OBJLC = Components.interfaces.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 setDisplay(obj, val) {
if (val)
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) {
setDisplay(obj, "none");
},
displayInherit: function(obj, state) {
setDisplay(obj, "inherit");
}
};
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
// Except for _loading...
let shouldSpawn = expectedMode == "plugin" && (!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.
if (expectedMode == "plugin") {
let scripted = false;
try {
let x = obj.getObjectValue();
scripted = true;
} catch(e) {}
ok(scripted, "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) {
for (let from_state of testdat['from_states']) {
for (let to_state of testdat['to_states']) {
for (let mode of testdat['test_modes']) {
for (let 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(function() SimpleTest.finish());
}
</script>

View File

@ -179,6 +179,13 @@ interface MozObjectLoadingContent {
[ChromeOnly]
readonly attribute unsigned long pluginFallbackType;
/**
* If this object currently owns a running plugin, regardless of whether or
* not one is pending spawn/despawn.
*/
[ChromeOnly]
readonly attribute boolean hasRunningPlugin;
/**
* This method will disable the play-preview plugin state.
*/