|
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"> |
|
10 |
|
11 "use strict"; |
|
12 SimpleTest.waitForExplicitFinish(); |
|
13 |
|
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 } |
|
22 |
|
23 // This can go away once embed also is on WebIDL |
|
24 let OBJLC = SpecialPowers.Ci.nsIObjectLoadingContent; |
|
25 |
|
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"; |
|
35 |
|
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; |
|
44 |
|
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 } |
|
71 |
|
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"); |
|
79 |
|
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 } |
|
124 |
|
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"; |
|
133 |
|
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 }; |
|
143 |
|
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 } |
|
164 |
|
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 }; |
|
252 |
|
253 |
|
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); |
|
259 |
|
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 } |
|
265 |
|
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 } |
|
272 |
|
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 } |
|
279 |
|
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 } |
|
286 |
|
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 } |
|
293 |
|
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 } |
|
300 |
|
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 } |
|
309 |
|
310 is(actualMode, expectedMode, "check loaded mode"); |
|
311 |
|
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); |
|
320 |
|
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"); |
|
329 |
|
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 } |
|
345 |
|
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); |
|
354 |
|
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); |
|
360 |
|
361 if (body.contains(obj)) |
|
362 body.removeChild(obj); |
|
363 |
|
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); |
|
377 |
|
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); |
|
399 |
|
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); |
|
421 |
|
422 if (body.contains(obj)) |
|
423 body.removeChild(obj); |
|
424 }); |
|
425 }, |
|
426 }; |
|
427 |
|
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 } |
|
457 |
|
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> |