Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Plugin instantiation</title>
5 <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
6 <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script>
7 <meta charset="utf-8">
8 <body onload="onLoad()">
9 <script class="testbody" type="text/javascript;version=1.8">
11 "use strict";
12 SimpleTest.waitForExplicitFinish();
14 var pluginHost = SpecialPowers.Cc["@mozilla.org/plugin/host;1"]
15 .getService(SpecialPowers.Ci.nsIPluginHost);
16 var pluginTags = pluginHost.getPluginTags();
17 for (var tag of pluginTags) {
18 if (tag.name == "Test Plug-in") {
19 tag.enabledState = SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED;;
20 }
21 }
23 // This can go away once embed also is on WebIDL
24 let OBJLC = SpecialPowers.Ci.nsIObjectLoadingContent;
26 // Use string modes in this test to make the test easier to read/debug.
27 // nsIObjectLoadingContent refers to this as "type", but I am using "mode"
28 // in the test to avoid confusing with content-type.
29 let prettyModes = {};
30 prettyModes[OBJLC.TYPE_LOADING] = "loading";
31 prettyModes[OBJLC.TYPE_IMAGE] = "image";
32 prettyModes[OBJLC.TYPE_PLUGIN] = "plugin";
33 prettyModes[OBJLC.TYPE_DOCUMENT] = "document";
34 prettyModes[OBJLC.TYPE_NULL] = "none";
36 let body = document.body;
37 // A single-pixel white png
38 let testPNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3AoIFiETNqbNRQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAACklEQVQIHWP4DwABAQEANl9ngAAAAABJRU5ErkJggg==';
39 // An empty, but valid, SVG
40 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>';
41 // executeSoon wrapper to count pending callbacks
42 let pendingCalls = 0;
43 let afterPendingCalls = false;
45 function runWhenDone(func) {
46 if (!pendingCalls)
47 func();
48 else
49 afterPendingCalls = func;
50 }
51 function runSoon(func) {
52 pendingCalls++;
53 SimpleTest.executeSoon(function() {
54 func();
55 if (--pendingCalls < 1 && afterPendingCalls)
56 afterPendingCalls();
57 });
58 }
59 function src(obj, state, uri) {
60 // If we have a running plugin, src changing should always throw it out,
61 // even if it causes us to load the same plugin again.
62 if (uri && runningPlugin(obj, state)) {
63 if (!state.oldPlugins)
64 state.oldPlugins = [];
65 try {
66 state.oldPlugins.push(obj.getObjectValue());
67 } catch (e) {
68 ok(false, "Running plugin but cannot call getObjectValue?");
69 }
70 }
72 var srcattr;
73 if (state.tagName == "object")
74 srcattr = "data";
75 else if (state.tagName == "embed")
76 srcattr = "src";
77 else
78 ok(false, "Internal test fail: Why are we setting the src of an applet");
80 // Plugins should always go away immediately on src/data change
81 state.initialPlugin = false;
82 if (uri === null) {
83 removeAttr(obj, srcattr);
84 // TODO Bug 767631 - we don't trigger loadObject on UnsetAttr :(
85 forceReload(obj, state);
86 } else {
87 setAttr(obj, srcattr, uri);
88 }
89 }
90 // We have to be careful not to reach past the nsObjectLoadingContent
91 // prototype to touch generic element attributes, as this will try to
92 // spawn the plugin, breaking our ability to test for that.
93 function getAttr(obj, attr) {
94 return document.body.constructor.prototype.getAttribute.call(obj, attr);
95 }
96 function setAttr(obj, attr, val) {
97 return document.body.constructor.prototype.setAttribute.call(obj, attr, val);
98 }
99 function hasAttr(obj, attr) {
100 return document.body.constructor.prototype.hasAttribute.call(obj, attr);
101 }
102 function removeAttr(obj, attr) {
103 return document.body.constructor.prototype.removeAttribute.call(obj, attr);
104 }
105 function setDisplayed(obj, display) {
106 if (display)
107 removeAttr(obj, 'style');
108 else
109 setAttr(obj, 'style', "display: none;");
110 }
111 function displayed(obj) {
112 // Hacky, but that's all we use style for.
113 return !hasAttr(obj, 'style');
114 }
115 function actualType(obj, state) {
116 return state.getActualType.call(obj);
117 }
118 function getMode(obj, state) {
119 return prettyModes[state.getDisplayedType.call(obj)];
120 }
121 function runningPlugin(obj, state) {
122 return state.getHasRunningPlugin.call(obj);
123 }
125 // TODO this is hacky and might hide some failures, but is needed until
126 // Bug 767635 lands -- which itself will probably mean tweaking this test.
127 function forceReload(obj, state) {
128 let attr;
129 if (state.tagName == "object")
130 attr = "data";
131 else if (state.tagName == "embed")
132 attr = "src";
134 if (attr && hasAttr(obj, attr)) {
135 src(obj, state, getAttr(obj, attr));
136 } else if (body.contains(obj)) {
137 body.appendChild(obj);
138 } else {
139 // Out of document nodes without data attributes simply can't be
140 // reloaded currently. Bug 767635
141 }
142 };
144 // Make a list of combinations of sub-lists, e.g.:
145 // [ [a, b], [c, d] ]
146 // ->
147 // [ [a, c], [a, d], [b, c], [b, d] ]
148 function eachList() {
149 let all = [];
150 if (!arguments.length)
151 return all;
152 let list = Array.prototype.slice.call(arguments, 0);
153 for (let c of list[0]) {
154 if (list.length > 1) {
155 for (let x of eachList.apply(this,list.slice(1))) {
156 all.push((c.length ? [c] : []).concat(x));
157 }
158 } else if (c.length) {
159 all.push([c]);
160 }
161 }
162 return all;
163 }
165 let states = {
166 svg: function(obj, state) {
167 removeAttr(obj, "type");
168 src(obj, state, testSVG);
169 state.noChannel = false;
170 state.expectedType = "image/svg";
171 // SVGs are actually image-like subdocuments
172 state.expectedMode = "document";
173 },
174 image: function(obj, state) {
175 removeAttr(obj, "type");
176 src(obj, state, testPNG);
177 state.noChannel = false;
178 state.expectedMode = "image";
179 state.expectedType = "image/png";
180 },
181 plugin: function(obj, state) {
182 removeAttr(obj, "type");
183 src(obj, state, "data:application/x-test,foo");
184 state.noChannel = false;
185 state.expectedType = "application/x-test";
186 state.expectedMode = "plugin";
187 },
188 pluginExtension: function(obj, state) {
189 src(obj, state, "./fake_plugin.tst");
190 state.expectedMode = "plugin";
191 state.pluginExtension = true;
192 state.noChannel = false;
193 },
194 document: function(obj, state) {
195 removeAttr(obj, "type");
196 src(obj, state, "data:text/plain,I am a document");
197 state.noChannel = false;
198 state.expectedType = "text/plain";
199 state.expectedMode = "document";
200 },
201 fallback: function(obj, state) {
202 removeAttr(obj, "type");
203 state.expectedType = "application/x-unknown";
204 state.expectedMode = "none";
205 state.noChannel = true;
206 src(obj, state, null);
207 },
208 addToDoc: function(obj, state) {
209 body.appendChild(obj);
210 },
211 removeFromDoc: function(obj, state) {
212 if (body.contains(obj))
213 body.removeChild(obj);
214 },
215 // Set the proper type
216 setType: function(obj, state) {
217 if (state.expectedType) {
218 state.badType = false;
219 setAttr(obj, 'type', state.expectedType);
220 forceReload(obj, state);
221 }
222 },
223 // Set an improper type
224 setWrongType: function(obj, state) {
225 // This should break no-channel-plugins but nothing else
226 state.badType = true;
227 setAttr(obj, 'type', "application/x-unknown");
228 forceReload(obj, state);
229 },
230 // Set a plugin type
231 setPluginType: function(obj, state) {
232 // If an object/embed has a type set to a plugin type, it should not
233 // use the channel type.
234 state.badType = false;
235 setAttr(obj, 'type', 'application/x-test');
236 state.expectedType = "application/x-test";
237 state.expectedMode = "plugin";
238 forceReload(obj, state);
239 },
240 noChannel: function(obj, state) {
241 src(obj, state, null);
242 state.noChannel = true;
243 state.pluginExtension = false;
244 },
245 displayNone: function(obj, state) {
246 setDisplayed(obj, false);
247 },
248 displayInherit: function(obj, state) {
249 setDisplayed(obj, true);
250 }
251 };
254 function testObject(obj, state) {
255 // If our test combination both sets noChannel but no explicit type
256 // it shouldn't load ever.
257 let expectedMode = state.expectedMode;
258 let actualMode = getMode(obj, state);
260 if (state.noChannel && !getAttr(obj, 'type')) {
261 // Some combinations of test both set no type and no channel. This is
262 // worth testing with the various combinations, but shouldn't load.
263 expectedMode = "none";
264 }
266 // Embed tags should always try to load a plugin by type or extension
267 // before falling back to opening a channel. See bug 803159
268 if (state.tagName == "embed" &&
269 (getAttr(obj, 'type') == "application/x-test" || state.pluginExtension)) {
270 state.noChannel = true;
271 }
273 // with state.loading, unless we're loading with no channel, these types
274 // should still be in loading state pending a channel.
275 if (state.loading && (expectedMode == "image" || expectedMode == "document" ||
276 (expectedMode == "plugin" && !state.initialPlugin && !state.noChannel))) {
277 expectedMode = "loading";
278 }
280 // With the exception of plugins with a proper type, nothing should
281 // load without a channel
282 if (state.noChannel && (expectedMode != "plugin" || state.badType) &&
283 body.contains(obj)) {
284 expectedMode = "none";
285 }
287 // embed tags should reject documents, except for SVG images which
288 // render as such
289 if (state.tagName == "embed" && expectedMode == "document" &&
290 actualType(obj, state) != "image/svg+xml") {
291 expectedMode = "none";
292 }
294 // Embeds with a plugin type should skip opening a channel prior to
295 // loading, taking only type into account.
296 if (state.tagName == 'embed' && getAttr(obj, 'type') == 'application/x-test' &&
297 body.contains(obj)) {
298 expectedMode = "plugin";
299 }
301 if (!body.contains(obj)
302 && (!state.loading || expectedMode != "image")
303 && (!state.initialPlugin || expectedMode != "plugin")) {
304 // Images are handled by nsIImageLoadingContent so we dont track
305 // their state change as they're detached and reattached. All other
306 // types switch to state "loading", and are completely unloaded
307 expectedMode = "loading";
308 }
310 is(actualMode, expectedMode, "check loaded mode");
312 // If we're a plugin, check that we spawned successfully. state.loading
313 // is set if we haven't had an event loop since applying state, in which
314 // case the plugin would not have stopped yet if it was initially a
315 // plugin.
316 let shouldBeSpawnable = expectedMode == "plugin" && displayed(obj);
317 let shouldSpawn = shouldBeSpawnable && (!state.loading || state.initialPlugin);
318 let didSpawn = runningPlugin(obj, state);
319 is(didSpawn, !!shouldSpawn, "check plugin spawned is " + !!shouldSpawn);
321 // If we are a plugin, scripting should work. If we're not spawned we
322 // should spawn synchronously.
323 let scripted = false;
324 try {
325 let x = obj.getObjectValue();
326 scripted = true;
327 } catch(e) {}
328 is(scripted, shouldBeSpawnable, "check plugin scriptability");
330 // If this tag previously had other spawned plugins, make sure it
331 // respawned between then and now
332 if (state.oldPlugins && didSpawn) {
333 let didRespawn = false;
334 for (let oldp of state.oldPlugins) {
335 // If this returns false or throws, it's not the same plugin
336 try {
337 didRespawn = !obj.checkObjectValue(oldp);
338 } catch (e) {
339 didRespawn = true;
340 }
341 }
342 is(didRespawn, true, "Plugin should have re-spawned since old state ("+state.oldPlugins.length+")");
343 }
344 }
346 let total = 0;
347 let test_modes = {
348 // Just apply from_state then to_state
349 "immediate": function(obj, from_state, to_state, state) {
350 for (let from of from_state)
351 states[from](obj, state);
352 for (let to of to_state)
353 states[to](obj, state);
355 // We don't spin the event loop between applying to_state and
356 // running tests, so some types are still loading
357 state.loading = true;
358 info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / immediate");
359 testObject(obj, state);
361 if (body.contains(obj))
362 body.removeChild(obj);
364 },
365 // Apply states, spin event loop, run tests.
366 "cycle": function(obj, from_state, to_state, state) {
367 for (let from of from_state)
368 states[from](obj, state);
369 for (let to of to_state)
370 states[to](obj, state);
371 // Because re-appending to the document creates a script blocker, but
372 // plugins spawn asynchronously, we need to return to the event loop
373 // twice to ensure the plugin has been given a chance to lazily spawn.
374 runSoon(function() { runSoon(function() {
375 info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cycle");
376 testObject(obj, state);
378 if (body.contains(obj))
379 body.removeChild(obj);
380 }); });
381 },
382 // Apply initial state, spin event loop, apply final state, spin event
383 // loop again.
384 "cycleboth": function(obj, from_state, to_state, state) {
385 for (let from of from_state) {
386 states[from](obj, state);
387 }
388 runSoon(function() {
389 for (let to of to_state) {
390 states[to](obj, state);
391 }
392 // Because re-appending to the document creates a script blocker,
393 // but plugins spawn asynchronously, we need to return to the event
394 // loop twice to ensure the plugin has been given a chance to lazily
395 // spawn.
396 runSoon(function() { runSoon(function() {
397 info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cycleboth");
398 testObject(obj, state);
400 if (body.contains(obj))
401 body.removeChild(obj);
402 }); });
403 });
404 },
405 // Apply initial state, spin event loop, apply later state, test
406 // immediately
407 "cyclefirst": function(obj, from_state, to_state, state) {
408 for (let from of from_state) {
409 states[from](obj, state);
410 }
411 runSoon(function() {
412 state.initialPlugin = runningPlugin(obj, state);
413 for (let to of to_state) {
414 states[to](obj, state);
415 }
416 info("["+(++total)+"] Testing [ " + from_state + " ] -> [ " + to_state + " ] / " + state.tagName + " / cyclefirst");
417 // We don't spin the event loop between applying to_state and
418 // running tests, so some types are still loading
419 state.loading = true;
420 testObject(obj, state);
422 if (body.contains(obj))
423 body.removeChild(obj);
424 });
425 },
426 };
428 function test(testdat) {
429 for (let from_state of testdat['from_states']) {
430 for (let to_state of testdat['to_states']) {
431 for (let mode of testdat['test_modes']) {
432 for (let type of testdat['tag_types']) {
433 runSoon(function () {
434 let obj = document.createElement(type);
435 obj.width = 1; obj.height = 1;
436 let state = {};
437 state.noChannel = true;
438 state.tagName = type;
439 // Part of the test checks whether a plugin spawned or not,
440 // but touching the object prototype will attempt to
441 // synchronously spawn a plugin! We use this terrible hack to
442 // get a privileged getter for the attributes we want to touch
443 // prior to applying any attributes.
444 // TODO when embed goes away we wont need to check for
445 // QueryInterface any longer.
446 var lookup_on = obj.QueryInterface ? obj.QueryInterface(OBJLC): obj;
447 state.getDisplayedType = SpecialPowers.do_lookupGetter(lookup_on, 'displayedType');
448 state.getHasRunningPlugin = SpecialPowers.do_lookupGetter(lookup_on, 'hasRunningPlugin');
449 state.getActualType = SpecialPowers.do_lookupGetter(lookup_on, 'actualType');
450 test_modes[mode](obj, from_state, to_state, state);
451 });
452 }
453 }
454 }
455 }
456 }
458 function onLoad() {
459 // Generic tests
460 test({
461 'tag_types': [ 'embed', 'object' ],
462 // In all three modes
463 'test_modes': [ 'immediate', 'cycle', 'cyclefirst', 'cycleboth' ],
464 // Starting from a blank tag in and out of the document, a loading
465 // plugin, and no-channel plugin (initial types only really have
466 // odd cases with plugins)
467 'from_states': [
468 [ 'addToDoc' ],
469 [ 'plugin' ],
470 [ 'plugin', 'addToDoc' ],
471 [ 'plugin', 'noChannel', 'setType', 'addToDoc' ],
472 [],
473 ],
474 // To various combinations of loaded objects
475 'to_states': eachList(
476 [ 'svg', 'image', 'plugin', 'document', '' ],
477 [ 'setType', 'setWrongType', 'setPluginType', '' ],
478 [ 'noChannel', '' ],
479 [ 'displayNone', 'displayInherit', '' ]
480 )});
481 // Special case test for embed tags with plugin-by-extension
482 // TODO object tags should be tested here too -- they have slightly
483 // different behavior, but waiting on a file load requires a loaded
484 // event handler and wont work with just our event loop spinning.
485 test({
486 'tag_types': [ 'embed' ],
487 'test_modes': [ 'immediate', 'cyclefirst', 'cycle', 'cycleboth' ],
488 'from_states': eachList(
489 [ 'svg', 'plugin', 'image', 'document' ],
490 [ 'addToDoc' ]
491 ),
492 // Set extension along with valid ty
493 'to_states': [
494 [ 'pluginExtension' ]
495 ]});
496 // Test plugin add/remove from document with adding/removing frame, with
497 // and without a channel.
498 test({
499 'tag_types': [ 'embed', 'object' ], // Ideally we'd test object too, but this gets exponentially long.
500 'test_modes': [ 'immediate', 'cyclefirst', 'cycle' ],
501 'from_states': [ [ 'displayNone', 'plugin', 'addToDoc' ],
502 [ 'displayNone', 'plugin', 'noChannel', 'addToDoc' ],
503 [ 'plugin', 'noChannel', 'addToDoc' ],
504 [ 'plugin', 'noChannel' ] ],
505 'to_states': eachList(
506 [ 'displayNone', '' ],
507 [ 'removeFromDoc' ],
508 [ 'image', 'displayNone', '' ],
509 [ 'image', 'displayNone', '' ],
510 [ 'addToDoc' ],
511 [ 'displayInherit' ]
512 )});
513 runWhenDone(function() SimpleTest.finish());
514 }
515 </script>