зеркало из https://github.com/mozilla/gecko-dev.git
511 строки
20 KiB
HTML
511 строки
20 KiB
HTML
<!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">
|
|
|
|
"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>
|