diff options
Diffstat (limited to 'devtools/server/tests/browser')
75 files changed, 5681 insertions, 0 deletions
diff --git a/devtools/server/tests/browser/.eslintrc.js b/devtools/server/tests/browser/.eslintrc.js new file mode 100644 index 000000000..c5b919ce3 --- /dev/null +++ b/devtools/server/tests/browser/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools mochitest eslintrc config. + "extends": "../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/server/tests/browser/animation.html b/devtools/server/tests/browser/animation.html new file mode 100644 index 000000000..d10a9873d --- /dev/null +++ b/devtools/server/tests/browser/animation.html @@ -0,0 +1,170 @@ +<!DOCTYPE html> +<style> + .not-animated { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: #eee; + } + + .simple-animation { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: red; + + animation: move 200s infinite; + } + + .multiple-animations { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: #eee; + + animation: move 200s infinite , glow 100s 5; + animation-timing-function: ease-out; + animation-direction: reverse; + animation-fill-mode: both; + } + + .transition { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: #f06; + + transition: width 500s ease-out; + } + .transition.get-round { + width: 200px; + } + + .long-animation { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: gold; + + animation: move 100s; + } + + .short-animation { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: purple; + + animation: move 1s; + } + + .delayed-animation { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: rebeccapurple; + + animation: move 200s 5s infinite; + } + + .delayed-transition { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: black; + + transition: width 500s 3s; + } + .delayed-transition.get-round { + width: 200px; + } + + .delayed-multiple-animations { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: green; + + animation: move .5s 1s 10, glow 1s .75s 30; + } + + .multiple-animations-2 { + display: inline-block; + + width: 50px; + height: 50px; + border-radius: 50%; + background: blue; + + animation: move .5s, glow 100s 2s infinite, grow 300s 1s 100; + } + + .all-transitions { + position: absolute; + top: 0; + right: 0; + width: 50px; + height: 50px; + background: blue; + transition: all .2s; + } + .all-transitions.expand { + width: 200px; + height: 100px; + } + + @keyframes move { + 100% { + transform: translateY(100px); + } + } + + @keyframes glow { + 100% { + background: yellow; + } + } + + @keyframes grow { + 100% { + width: 100px; + } + } +</style> +<div class="not-animated"></div> +<div class="simple-animation"></div> +<div class="multiple-animations"></div> +<div class="transition"></div> +<div class="long-animation"></div> +<div class="short-animation"></div> +<div class="delayed-animation"></div> +<div class="delayed-transition"></div> +<div class="delayed-multiple-animations"></div> +<div class="multiple-animations-2"></div> +<div class="all-transitions"></div> +<script type="text/javascript"> + // Get the transitions started when the page loads + var players; + addEventListener("load", function() { + document.querySelector(".transition").classList.add("get-round"); + document.querySelector(".delayed-transition").classList.add("get-round"); + }); +</script> diff --git a/devtools/server/tests/browser/browser.ini b/devtools/server/tests/browser/browser.ini new file mode 100644 index 000000000..c05933230 --- /dev/null +++ b/devtools/server/tests/browser/browser.ini @@ -0,0 +1,97 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + animation.html + doc_allocations.html + doc_force_cc.html + doc_force_gc.html + doc_innerHTML.html + doc_perf.html + navigate-first.html + navigate-second.html + storage-dynamic-windows.html + storage-listings.html + storage-unsecured-iframe.html + storage-updates.html + storage-secured-iframe.html + stylesheets-nested-iframes.html + timeline-iframe-child.html + timeline-iframe-parent.html + director-script-target.html + storage-helpers.js + !/devtools/server/tests/mochitest/hello-actor.js + +[browser_animation_emitMutations.js] +[browser_animation_getFrames.js] +[browser_animation_getProperties.js] +[browser_animation_getMultipleStates.js] +[browser_animation_getPlayers.js] +[browser_animation_getStateAfterFinished.js] +[browser_animation_getSubTreeAnimations.js] +[browser_animation_keepFinished.js] +[browser_animation_playerState.js] +[browser_animation_playPauseIframe.js] +[browser_animation_playPauseSeveral.js] +[browser_animation_reconstructState.js] +[browser_animation_refreshTransitions.js] +[browser_animation_setCurrentTime.js] +[browser_animation_setPlaybackRate.js] +[browser_animation_simple.js] +[browser_animation_updatedState.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_01.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_02.js] +[browser_canvasframe_helper_03.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_04.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_05.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_canvasframe_helper_06.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_markers-cycle-collection.js] +[browser_markers-docloading-01.js] +[browser_markers-docloading-02.js] +[browser_markers-docloading-03.js] +[browser_markers-gc.js] +[browser_markers-minor-gc.js] +[browser_markers-parse-html.js] +[browser_markers-styles.js] +[browser_markers-timestamp.js] +[browser_navigateEvents.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-allocation-data.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-profiler-01.js] +[browser_perf-profiler-02.js] +skip-if = true # Needs to be updated for async actor destruction +[browser_perf-profiler-03.js] +skip-if = true # Needs to be updated for async actor destruction +[browser_perf-realtime-markers.js] +[browser_perf-recording-actor-01.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-recording-actor-02.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_perf-samples-01.js] +[browser_perf-samples-02.js] +#[browser_perf-front-profiler-01.js] bug 1077464 +#[browser_perf-front-profiler-05.js] bug 1077464 +#[browser_perf-front-profiler-06.js] +[browser_storage_dynamic_windows.js] +[browser_storage_listings.js] +[browser_storage_updates.js] +[browser_stylesheets_getTextEmpty.js] +[browser_stylesheets_nested-iframes.js] +[browser_timeline.js] +[browser_timeline_actors.js] +[browser_timeline_iframes.js] +[browser_directorscript_actors_exports.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_directorscript_actors_error_events.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_directorscript_actors.js] +skip-if = e10s # Bug 1183605 - devtools/server/tests/browser/ tests are still disabled in E10S +[browser_register_actor.js] diff --git a/devtools/server/tests/browser/browser_animation_emitMutations.js b/devtools/server/tests/browser/browser_animation_emitMutations.js new file mode 100644 index 000000000..4783a8209 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_emitMutations.js @@ -0,0 +1,62 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the AnimationsActor emits events about changed animations on a +// node after getAnimationPlayersForNode was called on that node. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve a non-animated node"); + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + + info("Retrieve the animation player for the node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The node has no animation players"); + + info("Listen for new animations"); + let onMutations = once(animations, "mutations"); + + info("Add a couple of animation on the node"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "multiple-animations"} + ]); + let changes = yield onMutations; + + ok(true, "The mutations event was emitted"); + is(changes.length, 2, "There are 2 changes in the mutation event"); + ok(changes.every(({type}) => type === "added"), "Both changes are additions"); + + let names = changes.map(c => c.player.initialState.name).sort(); + is(names[0], "glow", "The animation 'glow' was added"); + is(names[1], "move", "The animation 'move' was added"); + + info("Store the 2 new players for comparing later"); + let p1 = changes[0].player; + let p2 = changes[1].player; + + info("Listen for removed animations"); + onMutations = once(animations, "mutations"); + + info("Remove the animation css class on the node"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "not-animated"} + ]); + + changes = yield onMutations; + + ok(true, "The mutations event was emitted"); + is(changes.length, 2, "There are 2 changes in the mutation event"); + ok(changes.every(({type}) => type === "removed"), "Both are removals"); + ok(changes[0].player === p1 || changes[0].player === p2, + "The first removed player was one of the previously added players"); + ok(changes[1].player === p1 || changes[1].player === p2, + "The second removed player was one of the previously added players"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_getFrames.js b/devtools/server/tests/browser/browser_animation_getFrames.js new file mode 100644 index 000000000..25ccfae3b --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getFrames.js @@ -0,0 +1,32 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationPlayerActor exposes a getFrames method that returns
+// the list of keyframes in the animation.
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+add_task(function* () {
+ let {client, walker, animations} =
+ yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
+
+ info("Get the test node and its animation front");
+ let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+ let [player] = yield animations.getAnimationPlayersForNode(node);
+
+ ok(player.getFrames, "The front has the getFrames method");
+
+ let frames = yield player.getFrames();
+ is(frames.length, 2, "The correct number of keyframes was retrieved");
+ ok(frames[0].transform, "Frame 0 has the transform property");
+ ok(frames[1].transform, "Frame 1 has the transform property");
+ // Note that we don't really test the content of the frame object here on
+ // purpose. This object comes straight out of the web animations API
+ // unmodified.
+
+ yield client.close();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_getMultipleStates.js b/devtools/server/tests/browser/browser_animation_getMultipleStates.js new file mode 100644 index 000000000..4436695b0 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getMultipleStates.js @@ -0,0 +1,55 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function* setup() { + yield SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processCount", 1]] + }); +}); + +// Check that the duration, iterationCount and delay are retrieved correctly for +// multiple animations. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playerHasAnInitialState(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playerHasAnInitialState(walker, animations) { + let state = yield getAnimationStateForNode(walker, animations, + ".delayed-multiple-animations", 0); + + ok(state.duration, 50000, + "The duration of the first animation is correct"); + ok(state.iterationCount, 10, + "The iterationCount of the first animation is correct"); + ok(state.delay, 1000, + "The delay of the first animation is correct"); + + state = yield getAnimationStateForNode(walker, animations, + ".delayed-multiple-animations", 1); + + ok(state.duration, 100000, + "The duration of the secon animation is correct"); + ok(state.iterationCount, 30, + "The iterationCount of the secon animation is correct"); + ok(state.delay, 750, + "The delay of the secon animation is correct"); +} + +function* getAnimationStateForNode(walker, animations, selector, playerIndex) { + let node = yield walker.querySelector(walker.rootNode, selector); + let players = yield animations.getAnimationPlayersForNode(node); + let player = players[playerIndex]; + yield player.ready(); + let state = yield player.getCurrentState(); + return state; +} diff --git a/devtools/server/tests/browser/browser_animation_getPlayers.js b/devtools/server/tests/browser/browser_animation_getPlayers.js new file mode 100644 index 000000000..a99a4dc4e --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getPlayers.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check the output of getAnimationPlayersForNode + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield theRightNumberOfPlayersIsReturned(walker, animations); + yield playersCanBePausedAndResumed(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* theRightNumberOfPlayersIsReturned(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, + "0 players were returned for the unanimated node"); + + node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 1, + "One animation player was returned"); + + node = yield walker.querySelector(walker.rootNode, ".multiple-animations"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 2, + "Two animation players were returned"); + + node = yield walker.querySelector(walker.rootNode, ".transition"); + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 1, + "One animation player was returned for the transitioned node"); +} + +function* playersCanBePausedAndResumed(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + yield player.ready(); + + ok(player.initialState, + "The player has an initialState"); + ok(player.getCurrentState, + "The player has the getCurrentState method"); + is(player.initialState.playState, "running", + "The animation is currently running"); + + yield player.pause(); + let state = yield player.getCurrentState(); + is(state.playState, "paused", + "The animation is now paused"); + + yield player.play(); + state = yield player.getCurrentState(); + is(state.playState, "running", + "The animation is now running again"); +} diff --git a/devtools/server/tests/browser/browser_animation_getProperties.js b/devtools/server/tests/browser/browser_animation_getProperties.js new file mode 100644 index 000000000..e95db544c --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getProperties.js @@ -0,0 +1,36 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the AnimationPlayerActor exposes a getProperties method that
+// returns the list of animated properties in the animation.
+
+const URL = MAIN_DOMAIN + "animation.html";
+
+add_task(function* () {
+ let {client, walker, animations} = yield initAnimationsFrontForUrl(URL);
+
+ info("Get the test node and its animation front");
+ let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+ let [player] = yield animations.getAnimationPlayersForNode(node);
+
+ ok(player.getProperties, "The front has the getProperties method");
+
+ let properties = yield player.getProperties();
+ is(properties.length, 1, "The correct number of properties was retrieved");
+
+ let propertyObject = properties[0];
+ is(propertyObject.name, "transform", "Property 0 is transform");
+
+ is(propertyObject.values.length, 2,
+ "The correct number of property values was retrieved");
+
+ // Note that we don't really test the content of the frame object here on
+ // purpose. This object comes straight out of the web animations API
+ // unmodified.
+
+ yield client.close();
+ gBrowser.removeCurrentTab();
+});
diff --git a/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js new file mode 100644 index 000000000..dd33237c1 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getStateAfterFinished.js @@ -0,0 +1,55 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the right duration/iterationCount/delay are retrieved even when +// the node has multiple animations and one of them already ended before getting +// the player objects. +// See devtools/server/actors/animation.js |getPlayerIndex| for more +// information. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve a non animated node"); + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + + info("Apply the multiple-animations-2 class to start the animations"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "multiple-animations-2"} + ]); + + info("Get the list of players, by the time this executes, the first, " + + "short, animation should have ended."); + let players = yield animations.getAnimationPlayersForNode(node); + if (players.length === 3) { + info("The short animation hasn't ended yet, wait for a bit."); + // The animation lasts for 500ms, so 1000ms should do it. + yield new Promise(resolve => setTimeout(resolve, 1000)); + + info("And get the list again"); + players = yield animations.getAnimationPlayersForNode(node); + } + + is(players.length, 2, "2 animations remain on the node"); + + is(players[0].state.duration, 100000, + "The duration of the first animation is correct"); + is(players[0].state.delay, 2000, + "The delay of the first animation is correct"); + is(players[0].state.iterationCount, null, + "The iterationCount of the first animation is correct"); + + is(players[1].state.duration, 300000, + "The duration of the second animation is correct"); + is(players[1].state.delay, 1000, + "The delay of the second animation is correct"); + is(players[1].state.iterationCount, 100, + "The iterationCount of the second animation is correct"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js new file mode 100644 index 000000000..50782d6de --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js @@ -0,0 +1,38 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationsActor can retrieve all animations inside a node's +// subtree (but not going into iframes). + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function* () { + info("Creating a test document with 2 iframes containing animated nodes"); + + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8," + + "<iframe id='iframe' src='" + URL + "'></iframe>"); + + info("Try retrieving all animations from the root doc's <body> node"); + let rootBody = yield walker.querySelector(walker.rootNode, "body"); + let players = yield animations.getAnimationPlayersForNode(rootBody); + is(players.length, 0, "The node has no animation players"); + + info("Retrieve all animations from the iframe's <body> node"); + let iframe = yield walker.querySelector(walker.rootNode, "#iframe"); + let {nodes} = yield walker.children(iframe); + let frameBody = yield walker.querySelector(nodes[0], "body"); + players = yield animations.getAnimationPlayersForNode(frameBody); + + // Testing for a hard-coded number of animations here would intermittently + // fail depending on how fast or slow the test is (indeed, the test page + // contains short transitions, and delayed animations). So just make sure we + // at least have the infinitely running animations. + ok(players.length >= 4, "All subtree animations were retrieved"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_keepFinished.js b/devtools/server/tests/browser/browser_animation_keepFinished.js new file mode 100644 index 000000000..a3240a5e0 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_keepFinished.js @@ -0,0 +1,54 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the AnimationsActor doesn't report finished animations as removed. +// Indeed, animations that only have the "finished" playState can be modified +// still, so we want the AnimationsActor to preserve the corresponding +// AnimationPlayerActor. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve a non-animated node"); + let node = yield walker.querySelector(walker.rootNode, ".not-animated"); + + info("Retrieve the animation player for the node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The node has no animation players"); + + info("Listen for new animations"); + let reportedMutations = []; + function onMutations(mutations) { + reportedMutations = [...reportedMutations, ...mutations]; + } + animations.on("mutations", onMutations); + + info("Add a short animation on the node"); + yield node.modifyAttributes([ + {attributeName: "class", newValue: "short-animation"} + ]); + + info("Wait for longer than the animation's duration"); + yield wait(2000); + + players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The added animation is surely finished"); + + is(reportedMutations.length, 1, "Only one mutation was reported"); + is(reportedMutations[0].type, "added", "The mutation was an addition"); + + animations.off("mutations", onMutations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} diff --git a/devtools/server/tests/browser/browser_animation_playPauseIframe.js b/devtools/server/tests/browser/browser_animation_playPauseIframe.js new file mode 100644 index 000000000..52320b84e --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_playPauseIframe.js @@ -0,0 +1,51 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationsActor can pause/play all animations even those +// within iframes. + +const URL = MAIN_DOMAIN + "animation.html"; + +add_task(function* () { + info("Creating a test document with 2 iframes containing animated nodes"); + + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8," + + "<iframe id='i1' src='" + URL + "'></iframe>" + + "<iframe id='i2' src='" + URL + "'></iframe>"); + + info("Getting the 2 iframe container nodes and animated nodes in them"); + let nodeInFrame1 = yield getNodeInFrame(walker, "#i1", ".simple-animation"); + let nodeInFrame2 = yield getNodeInFrame(walker, "#i2", ".simple-animation"); + + info("Pause all animations in the test document"); + yield animations.pauseAll(); + yield checkState(animations, nodeInFrame1, "paused"); + yield checkState(animations, nodeInFrame2, "paused"); + + info("Play all animations in the test document"); + yield animations.playAll(); + yield checkState(animations, nodeInFrame1, "running"); + yield checkState(animations, nodeInFrame2, "running"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* checkState(animations, nodeFront, playState) { + info("Getting the AnimationPlayerFront for the test node"); + let [player] = yield animations.getAnimationPlayersForNode(nodeFront); + yield player.ready; + let state = yield player.getCurrentState(); + is(state.playState, playState, + "The playState of the test node is " + playState); +} + +function* getNodeInFrame(walker, frameSelector, nodeSelector) { + let iframe = yield walker.querySelector(walker.rootNode, frameSelector); + let {nodes} = yield walker.children(iframe); + return walker.querySelector(nodes[0], nodeSelector); +} diff --git a/devtools/server/tests/browser/browser_animation_playPauseSeveral.js b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js new file mode 100644 index 000000000..9c52b5f57 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_playPauseSeveral.js @@ -0,0 +1,92 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the AnimationsActor can pause/play all animations at once, and +// check that it can also pause/play a given list of animations at once. + +// List of selectors that match "all" animated nodes in the test page. +// This list misses a bunch of animated nodes on purpose. Only the ones that +// have infinite animations are listed. This is done to avoid intermittents +// caused when finite animations are already done playing by the time the test +// runs. +const ALL_ANIMATED_NODES = [".simple-animation", ".multiple-animations", + ".delayed-animation"]; + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Pause all animations in the test document"); + yield animations.pauseAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused"); + + info("Play all animations in the test document"); + yield animations.playAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); + + info("Pause all animations in the test document using toggleAll"); + yield animations.toggleAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "paused"); + + info("Play all animations in the test document using toggleAll"); + yield animations.toggleAll(); + yield checkStates(walker, animations, ALL_ANIMATED_NODES, "running"); + + info("Play all animations from multiple animated node using toggleSeveral"); + let players = yield getPlayersFor(walker, animations, + [".multiple-animations"]); + is(players.length, 2, "Node has 2 animation players"); + yield animations.toggleSeveral(players, false); + let state1 = yield players[0].getCurrentState(); + is(state1.playState, "running", + "The playState of the first player is running"); + let state2 = yield players[1].getCurrentState(); + is(state2.playState, "running", + "The playState of the second player is running"); + + info("Pause one animation from a multiple animated node using toggleSeveral"); + yield animations.toggleSeveral([players[0]], true); + state1 = yield players[0].getCurrentState(); + is(state1.playState, "paused", "The playState of the first player is paused"); + state2 = yield players[1].getCurrentState(); + is(state2.playState, "running", + "The playState of the second player is running"); + + info("Play the same animation"); + yield animations.toggleSeveral([players[0]], false); + state1 = yield players[0].getCurrentState(); + is(state1.playState, "running", + "The playState of the first player is running"); + state2 = yield players[1].getCurrentState(); + is(state2.playState, "running", + "The playState of the second player is running"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* checkStates(walker, animations, selectors, playState) { + info("Checking the playState of all the nodes that have infinite running " + + "animations"); + + for (let selector of selectors) { + info("Getting the AnimationPlayerFront for node " + selector); + let [player] = yield getPlayersFor(walker, animations, selector); + yield player.ready(); + yield checkPlayState(player, selector, playState); + } +} + +function* getPlayersFor(walker, animations, selector) { + let node = yield walker.querySelector(walker.rootNode, selector); + return animations.getAnimationPlayersForNode(node); +} + +function* checkPlayState(player, selector, expectedState) { + let state = yield player.getCurrentState(); + is(state.playState, expectedState, + "The playState of node " + selector + " is " + expectedState); +} diff --git a/devtools/server/tests/browser/browser_animation_playerState.js b/devtools/server/tests/browser/browser_animation_playerState.js new file mode 100644 index 000000000..ac5842e39 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_playerState.js @@ -0,0 +1,123 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check the animation player's initial state + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playerHasAnInitialState(walker, animations); + yield playerStateIsCorrect(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playerHasAnInitialState(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.initialState, "The player front has an initial state"); + ok("startTime" in player.initialState, "Player's state has startTime"); + ok("currentTime" in player.initialState, "Player's state has currentTime"); + ok("playState" in player.initialState, "Player's state has playState"); + ok("playbackRate" in player.initialState, "Player's state has playbackRate"); + ok("name" in player.initialState, "Player's state has name"); + ok("duration" in player.initialState, "Player's state has duration"); + ok("delay" in player.initialState, "Player's state has delay"); + ok("iterationCount" in player.initialState, + "Player's state has iterationCount"); + ok("fill" in player.initialState, "Player's state has fill"); + ok("easing" in player.initialState, "Player's state has easing"); + ok("direction" in player.initialState, "Player's state has direction"); + ok("isRunningOnCompositor" in player.initialState, + "Player's state has isRunningOnCompositor"); + ok("type" in player.initialState, "Player's state has type"); + ok("documentCurrentTime" in player.initialState, + "Player's state has documentCurrentTime"); +} + +function* playerStateIsCorrect(walker, animations) { + info("Checking the state of the simple animation"); + + let player = yield getAnimationPlayerForNode(walker, animations, + ".simple-animation", 0); + let state = yield player.getCurrentState(); + is(state.name, "move", "Name is correct"); + is(state.duration, 200000, "Duration is correct"); + // null = infinite count + is(state.iterationCount, null, "Iteration count is correct"); + is(state.fill, "none", "Fill is correct"); + is(state.easing, "linear", "Easing is correct"); + is(state.direction, "normal", "Direction is correct"); + is(state.playState, "running", "PlayState is correct"); + is(state.playbackRate, 1, "PlaybackRate is correct"); + is(state.type, "cssanimation", "Type is correct"); + + info("Checking the state of the transition"); + + player = + yield getAnimationPlayerForNode(walker, animations, ".transition", 0); + state = yield player.getCurrentState(); + is(state.name, "width", "Transition name matches transition property"); + is(state.duration, 500000, "Transition duration is correct"); + // transitions run only once + is(state.iterationCount, 1, "Transition iteration count is correct"); + is(state.fill, "backwards", "Transition fill is correct"); + is(state.easing, "linear", "Transition easing is correct"); + is(state.direction, "normal", "Transition direction is correct"); + is(state.playState, "running", "Transition playState is correct"); + is(state.playbackRate, 1, "Transition playbackRate is correct"); + is(state.type, "csstransition", "Transition type is correct"); + // chech easing in keyframe + let keyframes = yield player.getFrames(); + is(keyframes.length, 2, "Transition length of keyframe is correct"); + is(keyframes[0].easing, + "ease-out", "Transition kerframes's easing is correct"); + + info("Checking the state of one of multiple animations on a node"); + + // Checking the 2nd player + player = yield getAnimationPlayerForNode(walker, animations, + ".multiple-animations", 1); + state = yield player.getCurrentState(); + is(state.name, "glow", "The 2nd animation's name is correct"); + is(state.duration, 100000, "The 2nd animation's duration is correct"); + is(state.iterationCount, 5, "The 2nd animation's iteration count is correct"); + is(state.fill, "both", "The 2nd animation's fill is correct"); + is(state.easing, "linear", "The 2nd animation's easing is correct"); + is(state.direction, "reverse", "The 2nd animation's direction is correct"); + is(state.playState, "running", "The 2nd animation's playState is correct"); + is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct"); + // chech easing in keyframe + keyframes = yield player.getFrames(); + is(keyframes.length, 2, "The 2nd animation's length of keyframe is correct"); + is(keyframes[0].easing, + "ease-out", "The 2nd animation's easing of kerframes is correct"); + + info("Checking the state of an animation with delay"); + + player = yield getAnimationPlayerForNode(walker, animations, + ".delayed-animation", 0); + state = yield player.getCurrentState(); + is(state.delay, 5000, "The animation delay is correct"); + + info("Checking the state of an transition with delay"); + + player = yield getAnimationPlayerForNode(walker, animations, + ".delayed-transition", 0); + state = yield player.getCurrentState(); + is(state.delay, 3000, "The transition delay is correct"); +} + +function* getAnimationPlayerForNode(walker, animations, nodeSelector, index) { + let node = yield walker.querySelector(walker.rootNode, nodeSelector); + let players = yield animations.getAnimationPlayersForNode(node); + let player = players[index]; + yield player.ready(); + return player; +} diff --git a/devtools/server/tests/browser/browser_animation_reconstructState.js b/devtools/server/tests/browser/browser_animation_reconstructState.js new file mode 100644 index 000000000..cd3007b86 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_reconstructState.js @@ -0,0 +1,38 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that, even though the AnimationPlayerActor only sends the bits of its +// state that change, the front reconstructs the whole state everytime. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playerHasCompleteStateAtAllTimes(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playerHasCompleteStateAtAllTimes(walker, animations) { + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + let [player] = yield animations.getAnimationPlayersForNode(node); + yield player.ready(); + + // Get the list of state key names from the initialstate. + let keys = Object.keys(player.initialState); + + // Get the state over and over again and check that the object returned + // contains all keys. + // Normally, only the currentTime will have changed in between 2 calls. + for (let i = 0; i < 10; i++) { + yield player.refreshState(); + keys.forEach(key => { + ok(typeof player.state[key] !== "undefined", + "The state retrieved has key " + key); + }); + } +} diff --git a/devtools/server/tests/browser/browser_animation_refreshTransitions.js b/devtools/server/tests/browser/browser_animation_refreshTransitions.js new file mode 100644 index 000000000..4cec0df69 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_refreshTransitions.js @@ -0,0 +1,77 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// When a transition finishes, no "removed" event is sent because it may still +// be used, but when it restarts again (transitions back), then a new +// AnimationPlayerFront should be sent, and the old one should be removed. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve the test node"); + let node = yield walker.querySelector(walker.rootNode, ".all-transitions"); + + info("Retrieve the animation players for the node"); + let players = yield animations.getAnimationPlayersForNode(node); + is(players.length, 0, "The node has no animation players yet"); + + info("Play a transition by adding the expand class, wait for mutations"); + let onMutations = expectMutationEvents(animations, 2); + let cpow = content.document.querySelector(".all-transitions"); + cpow.classList.add("expand"); + let reportedMutations = yield onMutations; + + is(reportedMutations.length, 2, "2 mutation events were received"); + is(reportedMutations[0].type, "added", "The first event was 'added'"); + is(reportedMutations[1].type, "added", "The second event was 'added'"); + + info("Wait for the transitions to be finished"); + yield waitForEnd(reportedMutations[0].player); + yield waitForEnd(reportedMutations[1].player); + + info("Play the transition back by removing the class, wait for mutations"); + onMutations = expectMutationEvents(animations, 4); + cpow.classList.remove("expand"); + reportedMutations = yield onMutations; + + is(reportedMutations.length, 4, "4 new mutation events were received"); + is(reportedMutations.filter(m => m.type === "removed").length, 2, + "2 'removed' events were sent (for the old transitions)"); + is(reportedMutations.filter(m => m.type === "added").length, 2, + "2 'added' events were sent (for the new transitions)"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function expectMutationEvents(animationsFront, nbOfEvents) { + return new Promise(resolve => { + let reportedMutations = []; + function onMutations(mutations) { + reportedMutations = [...reportedMutations, ...mutations]; + info("Received " + reportedMutations.length + " mutation events, " + + "expecting " + nbOfEvents); + if (reportedMutations.length === nbOfEvents) { + animationsFront.off("mutations", onMutations); + resolve(reportedMutations); + } + } + + info("Start listening for mutation events from the AnimationsFront"); + animationsFront.on("mutations", onMutations); + }); +} + +function* waitForEnd(animationFront) { + let playState; + while (playState !== "finished") { + let state = yield animationFront.getCurrentState(); + playState = state.playState; + info("Wait for transition " + animationFront.state.name + + " to finish, playState=" + playState); + } +} diff --git a/devtools/server/tests/browser/browser_animation_setCurrentTime.js b/devtools/server/tests/browser/browser_animation_setCurrentTime.js new file mode 100644 index 000000000..16dbaa544 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_setCurrentTime.js @@ -0,0 +1,74 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that a player's currentTime can be changed and that the AnimationsActor +// allows changing many players' currentTimes at once. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield testSetCurrentTime(walker, animations); + yield testSetCurrentTimes(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* testSetCurrentTime(walker, animations) { + info("Retrieve an animated node"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + + info("Retrieve the animation player for the node"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.setCurrentTime, "Player has the setCurrentTime method"); + + info("Check that the setCurrentTime method can be called"); + // Note that we don't check that it sets the animation to the right time here, + // this is too prone to intermittent failures, we'll do this later after + // pausing the animation. Here we merely test that the method doesn't fail. + yield player.setCurrentTime(player.initialState.currentTime + 1000); + + info("Pause the animation so we can really test if setCurrentTime works"); + yield player.pause(); + let pausedState = yield player.getCurrentState(); + + info("Set the current time to currentTime + 5s"); + yield player.setCurrentTime(pausedState.currentTime + 5000); + + let updatedState1 = yield player.getCurrentState(); + is(Math.round(updatedState1.currentTime - pausedState.currentTime), 5000, + "The currentTime was updated to +5s"); + + info("Set the current time to currentTime - 2s"); + yield player.setCurrentTime(updatedState1.currentTime - 2000); + let updatedState2 = yield player.getCurrentState(); + is(Math.round(updatedState2.currentTime - updatedState1.currentTime), -2000, + "The currentTime was updated to -2s"); +} + +function* testSetCurrentTimes(walker, animations) { + ok(animations.setCurrentTimes, "The AnimationsActor has the right method"); + + info("Retrieve multiple animated node and its animation players"); + + let nodeMulti = yield walker.querySelector(walker.rootNode, + ".multiple-animations"); + let players = (yield animations.getAnimationPlayersForNode(nodeMulti)); + + ok(players.length > 1, "Node has more than 1 animation player"); + + info("Try to set multiple current times at once"); + yield animations.setCurrentTimes(players, 500, true); + + info("Get the states of players and verify their correctness"); + for (let i = 0; i < players.length; i++) { + let state = yield players[i].getCurrentState(); + is(state.playState, "paused", `Player ${i + 1} is paused`); + is(state.currentTime, 500, `Player ${i + 1} has the right currentTime`); + } +} diff --git a/devtools/server/tests/browser/browser_animation_setPlaybackRate.js b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js new file mode 100644 index 000000000..b6d41b51e --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_setPlaybackRate.js @@ -0,0 +1,51 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that a player's playbackRate can be changed, and that multiple players +// can have their rates changed at the same time. + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + info("Retrieve an animated node"); + let node = yield walker.querySelector(walker.rootNode, ".simple-animation"); + + info("Retrieve the animation player for the node"); + let [player] = yield animations.getAnimationPlayersForNode(node); + + ok(player.setPlaybackRate, "Player has the setPlaybackRate method"); + + info("Change the rate to 10"); + yield player.setPlaybackRate(10); + + info("Query the state again"); + let state = yield player.getCurrentState(); + is(state.playbackRate, 10, "The playbackRate was updated"); + + info("Change the rate back to 1"); + yield player.setPlaybackRate(1); + + info("Query the state again"); + state = yield player.getCurrentState(); + is(state.playbackRate, 1, "The playbackRate was changed back"); + + info("Retrieve several animation players and set their rates"); + node = yield walker.querySelector(walker.rootNode, "body"); + let players = yield animations.getAnimationPlayersForNode(node); + + info("Change all animations in <body> to .5 rate"); + yield animations.setPlaybackRates(players, .5); + + info("Query their states and check they are correct"); + for (let player of players) { + let state = yield player.getCurrentState(); + is(state.playbackRate, .5, "The playbackRate was updated"); + } + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_simple.js b/devtools/server/tests/browser/browser_animation_simple.js new file mode 100644 index 000000000..52daf0084 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_simple.js @@ -0,0 +1,35 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Simple checks for the AnimationsActor + +add_task(function* () { + let {client, walker, animations} = yield initAnimationsFrontForUrl( + "data:text/html;charset=utf-8,<title>test</title><div></div>"); + + ok(animations, "The AnimationsFront was created"); + ok(animations.getAnimationPlayersForNode, + "The getAnimationPlayersForNode method exists"); + ok(animations.toggleAll, "The toggleAll method exists"); + ok(animations.playAll, "The playAll method exists"); + ok(animations.pauseAll, "The pauseAll method exists"); + + let didThrow = false; + try { + yield animations.getAnimationPlayersForNode(null); + } catch (e) { + didThrow = true; + } + ok(didThrow, "An exception was thrown for a missing NodeActor"); + + let invalidNode = yield walker.querySelector(walker.rootNode, "title"); + let players = yield animations.getAnimationPlayersForNode(invalidNode); + ok(Array.isArray(players), "An array of players was returned"); + is(players.length, 0, "0 players have been returned for the invalid node"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_animation_updatedState.js b/devtools/server/tests/browser/browser_animation_updatedState.js new file mode 100644 index 000000000..17d68e9e5 --- /dev/null +++ b/devtools/server/tests/browser/browser_animation_updatedState.js @@ -0,0 +1,55 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check the animation player's updated state + +add_task(function* () { + let {client, walker, animations} = + yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html"); + + yield playStateIsUpdatedDynamically(walker, animations); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function* playStateIsUpdatedDynamically(walker, animations) { + info("Getting the test node (which runs a very long animation)"); + // The animation lasts for 100s, to avoid intermittents. + let node = yield walker.querySelector(walker.rootNode, ".long-animation"); + + info("Getting the animation player front for this node"); + let [player] = yield animations.getAnimationPlayersForNode(node); + yield player.ready(); + + let state = yield player.getCurrentState(); + is(state.playState, "running", + "The playState is running while the animation is running"); + + info("Change the animation's currentTime to be near the end and wait for " + + "it to finish"); + let onFinished = waitForAnimationPlayState(player, "finished"); + // Set the currentTime to 98s, knowing that the animation lasts for 100s. + yield player.setCurrentTime(98 * 1000); + state = yield onFinished; + is(state.playState, "finished", + "The animation has ended and the state has been updated"); + ok(state.currentTime > player.initialState.currentTime, + "The currentTime has been updated"); +} + +function* waitForAnimationPlayState(player, playState) { + let state = {}; + while (state.playState !== playState) { + state = yield player.getCurrentState(); + yield wait(500); + } + return state; +} + +function wait(ms) { + return new Promise(r => setTimeout(r, ms)); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_01.js b/devtools/server/tests/browser/browser_canvasframe_helper_01.js new file mode 100644 index 000000000..7fd943197 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_01.js @@ -0,0 +1,90 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Simple CanvasFrameAnonymousContentHelper tests. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + child.textContent = "test element"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + ok(helper.content instanceof AnonymousContent, + "The helper owns the AnonymousContent object"); + ok(helper.getTextContentForElement, + "The helper has the getTextContentForElement method"); + ok(helper.setTextContentForElement, + "The helper has the setTextContentForElement method"); + ok(helper.setAttributeForElement, + "The helper has the setAttributeForElement method"); + ok(helper.getAttributeForElement, + "The helper has the getAttributeForElement method"); + ok(helper.removeAttributeForElement, + "The helper has the removeAttributeForElement method"); + ok(helper.addEventListenerForElement, + "The helper has the addEventListenerForElement method"); + ok(helper.removeEventListenerForElement, + "The helper has the removeEventListenerForElement method"); + ok(helper.getElement, + "The helper has the getElement method"); + ok(helper.scaleRootElement, + "The helper has the scaleRootElement method"); + + is(helper.getTextContentForElement("child-element"), "test element", + "The text content was retrieve correctly"); + is(helper.getAttributeForElement("child-element", "id"), "child-element", + "The ID attribute was retrieve correctly"); + is(helper.getAttributeForElement("child-element", "class"), "child-element", + "The class attribute was retrieve correctly"); + + let el = helper.getElement("child-element"); + ok(el, "The DOMNode-like element was created"); + + is(el.getTextContent(), "test element", + "The text content was retrieve correctly"); + is(el.getAttribute("id"), "child-element", + "The ID attribute was retrieve correctly"); + is(el.getAttribute("class"), "child-element", + "The class attribute was retrieve correctly"); + + info("Destroying the helper"); + helper.destroy(); + env.destroy(); + + ok(!helper.getTextContentForElement("child-element"), + "No text content was retrieved after the helper was destroyed"); + ok(!helper.getAttributeForElement("child-element", "id"), + "No ID attribute was retrieved after the helper was destroyed"); + ok(!helper.getAttributeForElement("child-element", "class"), + "No class attribute was retrieved after the helper was destroyed"); + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_02.js b/devtools/server/tests/browser/browser_canvasframe_helper_02.js new file mode 100644 index 000000000..90400c50a --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_02.js @@ -0,0 +1,48 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the CanvasFrameAnonymousContentHelper does not insert content in +// XUL windows. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +add_task(function* () { + let browser = yield addTab("about:preferences"); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + child.textContent = "test element"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + ok(!helper.content, "The AnonymousContent was not inserted in the window"); + ok(!helper.getTextContentForElement("child-element"), + "No text content is returned"); + + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_03.js b/devtools/server/tests/browser/browser_canvasframe_helper_03.js new file mode 100644 index 000000000..85e27c7de --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_03.js @@ -0,0 +1,102 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the CanvasFrameAnonymousContentHelper event handling mechanism. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + let el = helper.getElement("child-element"); + + info("Adding an event listener on the inserted element"); + let mouseDownHandled = 0; + function onMouseDown(e, id) { + is(id, "child-element", "The mousedown event was triggered on the element"); + ok(!e.originalTarget, "The originalTarget property isn't available"); + mouseDownHandled++; + } + el.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the inserted element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, "The mousedown event was handled once on the element"); + + info("Synthesizing an event somewhere else"); + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(400, 400, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, "The mousedown event was not handled on the element"); + + info("Removing the event listener"); + el.removeEventListener("mousedown", onMouseDown); + + info("Synthesizing another event after the listener has been removed"); + // Using a document event listener to know when the event has been synthesized. + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, + "The mousedown event hasn't been handled after the listener was removed"); + + info("Adding again the event listener"); + el.addEventListener("mousedown", onMouseDown); + + info("Destroying the helper"); + env.destroy(); + helper.destroy(); + + info("Synthesizing another event after the helper has been destroyed"); + // Using a document event listener to know when the event has been synthesized. + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled, 1, + "The mousedown event hasn't been handled after the helper was destroyed"); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_04.js b/devtools/server/tests/browser/browser_canvasframe_helper_04.js new file mode 100644 index 000000000..d038f84a0 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_04.js @@ -0,0 +1,98 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the CanvasFrameAnonymousContentHelper re-inserts the content when the +// page reloads. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); +const events = require("sdk/event/core"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL_1 = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 1"; +const TEST_URL_2 = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test 2"; + +add_task(function* () { + let browser = yield addTab(TEST_URL_2); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + child.className = "child-element"; + child.textContent = "test content"; + root.appendChild(child); + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + info("Get an element from the helper"); + let el = helper.getElement("child-element"); + + info("Try to access the element"); + is(el.getAttribute("class"), "child-element", + "The attribute is correct before navigation"); + is(el.getTextContent(), "test content", + "The text content is correct before navigation"); + + info("Add an event listener on the element"); + let mouseDownHandled = 0; + function onMouseDown(e, id) { + is(id, "child-element", "The mousedown event was triggered on the element"); + mouseDownHandled++; + } + el.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + is(mouseDownHandled, 1, "The mousedown event was handled once before navigation"); + + info("Navigating to a new page"); + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + content.location = TEST_URL_2; + yield loaded; + doc = gBrowser.selectedBrowser.contentWindow.document; + + info("Try to access the element again"); + is(el.getAttribute("class"), "child-element", + "The attribute is correct after navigation"); + is(el.getTextContent(), "test content", + "The text content is correct after navigation"); + + info("Synthesizing an event on the element again"); + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + is(mouseDownHandled, 1, "The mousedown event was not handled after navigation"); + + info("Destroying the helper"); + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_05.js b/devtools/server/tests/browser/browser_canvasframe_helper_05.js new file mode 100644 index 000000000..94fb66914 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_05.js @@ -0,0 +1,112 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test some edge cases of the CanvasFrameAnonymousContentHelper event handling +// mechanism. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + + let parent = doc.createElement("div"); + parent.style = "pointer-events:auto;width:300px;height:300px;background:yellow;"; + parent.id = "parent-element"; + root.appendChild(parent); + + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + parent.appendChild(child); + + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + info("Getting the parent and child elements"); + let parentEl = helper.getElement("parent-element"); + let childEl = helper.getElement("child-element"); + + info("Adding an event listener on both elements"); + let mouseDownHandled = []; + function onMouseDown(e, id) { + mouseDownHandled.push(id); + } + parentEl.addEventListener("mousedown", onMouseDown); + childEl.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the child element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 2, "The mousedown event was handled twice"); + is(mouseDownHandled[0], "child-element", + "The mousedown event was handled on the child element"); + is(mouseDownHandled[1], "parent-element", + "The mousedown event was handled on the parent element"); + + info("Synthesizing an event on the parent, outside of the child element"); + mouseDownHandled = []; + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(250, 250, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled only once"); + is(mouseDownHandled[0], "parent-element", + "The mousedown event was handled on the parent element"); + + info("Removing the event listener"); + parentEl.removeEventListener("mousedown", onMouseDown); + childEl.removeEventListener("mousedown", onMouseDown); + + info("Adding an event listener on the parent element only"); + mouseDownHandled = []; + parentEl.addEventListener("mousedown", onMouseDown); + + info("Synthesizing an event on the child element"); + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled once"); + is(mouseDownHandled[0], "parent-element", + "The mousedown event did bubble to the parent element"); + + info("Removing the parent listener"); + parentEl.removeEventListener("mousedown", onMouseDown); + + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_06.js b/devtools/server/tests/browser/browser_canvasframe_helper_06.js new file mode 100644 index 000000000..2b137fe26 --- /dev/null +++ b/devtools/server/tests/browser/browser_canvasframe_helper_06.js @@ -0,0 +1,100 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test support for event propagation stop in the +// CanvasFrameAnonymousContentHelper event handling mechanism. + +// This makes sure the 'domnode' protocol actor type is known when importing +// highlighter. +require("devtools/server/actors/inspector"); + +const {HighlighterEnvironment} = require("devtools/server/actors/highlighters"); + +const { + CanvasFrameAnonymousContentHelper +} = require("devtools/server/actors/highlighters/utils/markup"); + +const TEST_URL = "data:text/html;charset=utf-8,CanvasFrameAnonymousContentHelper test"; + +add_task(function* () { + let browser = yield addTab(TEST_URL); + let doc = browser.contentDocument; + + let nodeBuilder = () => { + let root = doc.createElement("div"); + + let parent = doc.createElement("div"); + parent.style = "pointer-events:auto;width:300px;height:300px;background:yellow;"; + parent.id = "parent-element"; + root.appendChild(parent); + + let child = doc.createElement("div"); + child.style = "pointer-events:auto;width:200px;height:200px;background:red;"; + child.id = "child-element"; + parent.appendChild(child); + + return root; + }; + + info("Building the helper"); + let env = new HighlighterEnvironment(); + env.initFromWindow(doc.defaultView); + let helper = new CanvasFrameAnonymousContentHelper(env, nodeBuilder); + + info("Getting the parent and child elements"); + let parentEl = helper.getElement("parent-element"); + let childEl = helper.getElement("child-element"); + + info("Adding an event listener on both elements"); + let mouseDownHandled = []; + + function onParentMouseDown(e, id) { + mouseDownHandled.push(id); + } + parentEl.addEventListener("mousedown", onParentMouseDown); + + function onChildMouseDown(e, id) { + mouseDownHandled.push(id); + e.stopPropagation(); + } + childEl.addEventListener("mousedown", onChildMouseDown); + + info("Synthesizing an event on the child element"); + let onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(100, 100, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled only once"); + is(mouseDownHandled[0], "child-element", + "The mousedown event was handled on the child element"); + + info("Synthesizing an event on the parent, outside of the child element"); + mouseDownHandled = []; + onDocMouseDown = once(doc, "mousedown"); + synthesizeMouseDown(250, 250, doc.defaultView); + yield onDocMouseDown; + + is(mouseDownHandled.length, 1, "The mousedown event was handled only once"); + is(mouseDownHandled[0], "parent-element", + "The mousedown event was handled on the parent element"); + + info("Removing the event listener"); + parentEl.removeEventListener("mousedown", onParentMouseDown); + childEl.removeEventListener("mousedown", onChildMouseDown); + + env.destroy(); + helper.destroy(); + + gBrowser.removeCurrentTab(); +}); + +function synthesizeMouseDown(x, y, win) { + // We need to make sure the inserted anonymous content can be targeted by the + // event right after having been inserted, and so we need to force a sync + // reflow. + let forceReflow = win.document.documentElement.offsetWidth; + EventUtils.synthesizeMouseAtPoint(x, y, {type: "mousedown"}, win); +} diff --git a/devtools/server/tests/browser/browser_directorscript_actors.js b/devtools/server/tests/browser/browser_directorscript_actors.js new file mode 100644 index 000000000..bdfc8f8f1 --- /dev/null +++ b/devtools/server/tests/browser/browser_directorscript_actors.js @@ -0,0 +1,159 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {DirectorManagerFront} = require("devtools/shared/fronts/director-manager"); +const {DirectorRegistry} = require("devtools/server/actors/director-registry"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "director-script-target.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + DirectorRegistry.clear(); + let directorManager = DirectorManagerFront(client, form); + + yield testDirectorScriptAttachEventAttributes(directorManager); + yield testDirectorScriptMessagePort(directorManager); + yield testDirectorScriptWindowEval(directorManager); + yield testDirectorScriptUnloadOnDetach(directorManager); + + yield client.close(); + gBrowser.removeCurrentTab(); + DirectorRegistry.clear(); +}); + +function* testDirectorScriptAttachEventAttributes(directorManager) { + let attachEvent = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_attachEventAttributes", + scriptCode: "(" + (function () { + exports.attach = function () {}; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + let { directorScriptId, url } = attachEvent; + + is(directorScriptId, "testDirectorScript_attachEventAttributes", + "attach event should contains directorScriptId"); + is(url, MAIN_DOMAIN + "director-script-target.html"); +} + +function* testDirectorScriptMessagePort(directorManager) { + let { port } = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_MessagePort", + scriptCode: "(" + (function () { + exports.attach = function ({port}) { + port.onmessage = function (evt) { + // echo messages + evt.source.postMessage(evt.data); + }; + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + ok(port && port.postMessage, "testDirector_MessagePort port received"); + + // exchange messages over the MessagePort + let waitForMessagePortEvent = once(port, "message"); + // needs to explicit start the port + port.start(); + + var msg = { k1: "v1", k2: [1, 2, 3] }; + port.postMessage(msg); + + var reply = yield waitForMessagePortEvent; + + is(JSON.stringify(reply.data), JSON.stringify(msg), "echo reply received on the MessagePortClient"); +} + +function* testDirectorScriptWindowEval(directorManager) { + let { port } = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_WindowEval", + scriptCode: "(" + (function () { + exports.attach = function ({window, port}) { + var onpageloaded = function () { + var globalVarValue = window.eval("globalAccessibleVar;"); + port.postMessage(globalVarValue); + }; + + if (window.document && window.document.readyState === "complete") { + onpageloaded(); + } else { + window.addEventListener("load", onpageloaded, false); + } + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + ok(port, "testDirectorScript_WindowEval port received"); + + // exchange messages over the MessagePort + let waitForMessagePortEvent = once(port, "message"); + // needs to explicit start the port + port.start(); + + var portEvent = yield waitForMessagePortEvent; + + ok(portEvent.data !== "unsecure-eval", "window.eval should be wrapped and safe"); + is(portEvent.data, "global-value", "globalAccessibleVar should be accessible through window.eval"); +} + +function* testDirectorScriptUnloadOnDetach(directorManager) { + let { port } = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_unloadOnDetach", + scriptCode: "(" + (function () { + exports.attach = function ({port, onUnload}) { + onUnload(function () { + port.postMessage("ONUNLOAD"); + }); + }; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + + ok(port, "testDirectorScript_unloadOnDetach port received"); + port.start(); + + let waitForDetach = once(directorManager, "director-script-detach"); + let waitForMessage = once(port, "message"); + + directorManager.disableByScriptIds(["testDirectorScript_unloadOnDetach"], {reload: false}); + + let { directorScriptId } = yield waitForDetach; + is(directorScriptId, "testDirectorScript_unloadOnDetach", + "detach event should contains directorScriptId"); + + let portEvent = yield waitForMessage; + is(portEvent.data, "ONUNLOAD", "director-script's exports.onUnload called on detach"); +} + +function* installAndEnableDirectorScript(directorManager, directorScriptDef) { + let { scriptId } = directorScriptDef; + + DirectorRegistry.install(scriptId, directorScriptDef); + + let waitForAttach = once(directorManager, "director-script-attach"); + let waitForError = once(directorManager, "director-script-error"); + + directorManager.enableByScriptIds([scriptId], {reload: false}); + + let attachOrErrorEvent = yield Promise.race([waitForAttach, waitForError]); + + return attachOrErrorEvent; +} diff --git a/devtools/server/tests/browser/browser_directorscript_actors_error_events.js b/devtools/server/tests/browser/browser_directorscript_actors_error_events.js new file mode 100644 index 000000000..0afe16388 --- /dev/null +++ b/devtools/server/tests/browser/browser_directorscript_actors_error_events.js @@ -0,0 +1,132 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {DirectorManagerFront} = require("devtools/shared/fronts/director-manager"); +const {DirectorRegistry} = require("devtools/server/actors/director-registry"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "director-script-target.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + DirectorRegistry.clear(); + let directorManager = DirectorManagerFront(client, form); + + yield testErrorOnRequire(directorManager); + yield testErrorOnEvaluate(directorManager); + yield testErrorOnAttach(directorManager); + yield testErrorOnDetach(directorManager); + + yield client.close(); + gBrowser.removeCurrentTab(); + DirectorRegistry.clear(); +}); + +function* testErrorOnRequire(directorManager) { + // director script require method should raise a "not implemented" exception + let errorOnRequire = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnRequire", + scriptCode: "(" + (function () { + // this director script should generate an error event + // because require raise a "not implemented" exception + require("fake_module"); + }).toString() + ")();", + scriptOptions: {} + }); + + assertIsDirectorScriptError(errorOnRequire); + + let { message } = errorOnRequire; + is(message, "Error: NOT IMPLEMENTED", "error.message contains the expected error message"); +} + +function* testErrorOnEvaluate(directorManager) { + // director scripts should send an error events if the director script raise an exception on + // evaluation + let errorOnEvaluate = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnEvaluate", + scriptCode: "(" + (function () { + // this will raise an exception evaluating + // the director script + raise.an_error.during.content_script.load(); + }).toString() + ")();", + scriptOptions: {} + }); + assertIsDirectorScriptError(errorOnEvaluate); +} + +function* testErrorOnAttach(directorManager) { + // director scripts should send an error events if the director script raise an exception on + // evaluation + let errorOnAttach = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnAttach", + scriptCode: "(" + (function () { + // this will raise an exception on evaluating + // the director script + module.exports = function () { + raise.an_error.during.content_script.load(); + }; + }).toString() + ")();", + scriptOptions: {} + }); + assertIsDirectorScriptError(errorOnAttach); +} + +function* testErrorOnDetach(directorManager) { + // director scripts should send an error events if the director script raise an exception on + // evaluation + let attach = yield installAndEnableDirectorScript(directorManager, { + scriptId: "testDirectorScript_errorOnDetach", + scriptCode: "(" + (function () { + module.exports = function ({onUnload}) { + // this will raise an exception on unload the director script + onUnload(function () { + raise_an_error_onunload(); + }); + }; + }).toString() + ")();", + scriptOptions: {} + }); + + let waitForDetach = once(directorManager, "director-script-detach"); + let waitForError = once(directorManager, "director-script-error"); + + directorManager.disableByScriptIds(["testDirectorScript_errorOnDetach"], {reload: false}); + + let detach = yield waitForDetach; + let error = yield waitForError; + ok(detach, "testDirectorScript_errorOnDetach detach event received"); + ok(error, "testDirectorScript_errorOnDetach detach error received"); + assertIsDirectorScriptError(error); +} + +function* installAndEnableDirectorScript(directorManager, directorScriptDef) { + let { scriptId } = directorScriptDef; + + DirectorRegistry.install(scriptId, directorScriptDef); + + let waitForAttach = once(directorManager, "director-script-attach"); + let waitForError = once(directorManager, "director-script-error"); + + directorManager.enableByScriptIds([scriptId], {reload: false}); + + let attachOrErrorEvent = yield Promise.race([waitForAttach, waitForError]); + + return attachOrErrorEvent; +} + +function assertIsDirectorScriptError(error) { + ok(!!error.message, "errors should contain a message"); + ok(!!error.stack, "errors should contain a stack trace"); + ok(!!error.fileName, "errors should contain a fileName"); + ok(typeof error.columnNumber == "number", "errors should contain a columnNumber"); + ok(typeof error.lineNumber == "number", "errors should contain a lineNumber"); + + ok(!!error.directorScriptId, "errors should contain a directorScriptId"); +} diff --git a/devtools/server/tests/browser/browser_directorscript_actors_exports.js b/devtools/server/tests/browser/browser_directorscript_actors_exports.js new file mode 100644 index 000000000..f9ef56f51 --- /dev/null +++ b/devtools/server/tests/browser/browser_directorscript_actors_exports.js @@ -0,0 +1,87 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {DirectorManagerFront} = require("devtools/shared/fronts/director-manager"); +const {DirectorRegistry} = require("devtools/server/actors/director-registry"); + +DirectorRegistry.clear(); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "director-script-target.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + DirectorRegistry.clear(); + let directorManager = DirectorManagerFront(client, form); + + // director scripts attach method defaults to module.exports + let attachModuleExports = yield testDirectorScriptExports(directorManager, { + scriptId: "testDirectorScript_moduleExports", + scriptCode: "(" + (function () { + module.exports = function () {}; + }).toString() + ")();", + scriptOptions: {} + }); + ok(attachModuleExports.port, "testDirectorScript_moduleExports attach event received"); + + // director scripts attach method can be configured using the attachMethod scriptOptions + let attachExportsAttach = yield testDirectorScriptExports(directorManager, { + scriptId: "testDirectorScript_exportsAttach", + scriptCode: "(" + (function () { + exports.attach = function () {}; + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + ok(attachExportsAttach.port, "testDirectorScript_exportsAttach attach event received"); + + // director scripts without an attach method generates an error event + let errorUndefinedAttachMethod = yield testDirectorScriptExports(directorManager, { + scriptId: "testDirectorScript_undefinedAttachMethod", + scriptCode: "(" + (function () { + // this director script should raise an error + // because it doesn't export any attach method + }).toString() + ")();", + scriptOptions: { + attachMethod: "attach" + } + }); + let { message } = errorUndefinedAttachMethod; + ok(!!message, "testDirectorScript_undefinedAttachMethod error event received"); + assertIsDirectorScriptError(errorUndefinedAttachMethod); + + yield client.close(); + gBrowser.removeCurrentTab(); + DirectorRegistry.clear(); +}); + +function assertIsDirectorScriptError(error) { + ok(!!error.message, "errors should contain a message"); + ok(!!error.stack, "errors should contain a stack trace"); + ok(!!error.fileName, "errors should contain a fileName"); + ok(typeof error.columnNumber == "number", "errors should contain a columnNumber"); + ok(typeof error.lineNumber == "number", "errors should contain a lineNumber"); + + ok(!!error.directorScriptId, "errors should contain a directorScriptId"); +} + +function* testDirectorScriptExports(directorManager, directorScriptDef) { + let { scriptId } = directorScriptDef; + + DirectorRegistry.install(scriptId, directorScriptDef); + + let waitForAttach = once(directorManager, "director-script-attach"); + let waitForError = once(directorManager, "director-script-error"); + directorManager.enableByScriptIds([scriptId], {reload: false}); + + let attachOrErrorEvent = yield Promise.race([waitForAttach, waitForError]); + + return attachOrErrorEvent; +} diff --git a/devtools/server/tests/browser/browser_markers-cycle-collection.js b/devtools/server/tests/browser/browser_markers-cycle-collection.js new file mode 100644 index 000000000..dc33375f2 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-cycle-collection.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "nsCycleCollector::Collect" and + * "nsCycleCollector::ForgetSkippable" markers when we force cycle collection. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + // This test runs very slowly on linux32 debug EC2 instances. + requestLongerTimeout(2); + + let browser = yield addTab(MAIN_DOMAIN + "doc_force_cc.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, ["nsCycleCollector::Collect", "nsCycleCollector::ForgetSkippable"]); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === "nsCycleCollector::Collect"), "got some nsCycleCollector::Collect markers"); + ok(markers.some(m => m.name === "nsCycleCollector::ForgetSkippable"), "got some nsCycleCollector::Collect markers"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-01.js b/devtools/server/tests/browser/browser_markers-docloading-01.js new file mode 100644 index 000000000..3c82d56c4 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-01.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withMarkers: true }); + + front.once("doc-loading", e => { + ok(false, "Should not be emitting doc-loading events."); + }); + + executeSoon(() => doc.location.reload()); + + yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers"); + yield front.stop(rec); + + ok(true, "Found the required marker names."); + + // Wait some more time to make sure the 'doc-loading' events never get fired. + yield DevToolsUtils.waitForTime(1000); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-02.js b/devtools/server/tests/browser/browser_markers-docloading-02.js new file mode 100644 index 000000000..0142ea0cd --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-02.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withMarkers: true, withDocLoadingEvents: true }); + + yield new Promise(resolve => { + front.once("doc-loading", resolve); + doc.location.reload(); + }); + + ok(true, "At least one doc-loading event got fired."); + + yield waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers"); + yield front.stop(rec); + + ok(true, "Found the required marker names."); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-docloading-03.js b/devtools/server/tests/browser/browser_markers-docloading-03.js new file mode 100644 index 000000000..1960da4da --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-docloading-03.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get DOMContentLoaded and Load markers + */ + +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"]; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + let rec = yield front.start({ withDocLoadingEvents: true }); + + waitForMarkerType(front, MARKER_NAMES, () => true, e => e, "markers").then(e => { + ok(false, "Should not be emitting doc-loading markers."); + }); + + yield new Promise(resolve => { + front.once("doc-loading", resolve); + doc.location.reload(); + }); + + ok(true, "At least one doc-loading event got fired."); + + yield front.stop(rec); + + // Wait some more time to make sure the 'doc-loading' markers never get fired. + yield DevToolsUtils.waitForTime(1000); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-gc.js b/devtools/server/tests/browser/browser_markers-gc.js new file mode 100644 index 000000000..559b19161 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-gc.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "GarbageCollection" markers. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const MARKER_NAME = "GarbageCollection"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_force_gc.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, MARKER_NAME); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`); + ok(markers.every(({causeName}) => typeof causeName === "string"), + "All markers have a causeName."); + ok(markers.every(({cycle}) => typeof cycle === "number"), + "All markers have a `cycle` ID."); + + markers = rec.getMarkers(); + + // Bug 1197646 + let ordered = true; + markers.reduce((previousStart, current, i) => { + if (i === 0) { + return current.start; + } + if (current.start < previousStart) { + ok(false, `markers must be in order. ${current.name} marker has later start time (${current.start}) thanprevious: ${previousStart}`); + ordered = false; + } + return current.start; + }); + + is(ordered, true, "All GC and non-GC markers are in order by start time."); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-minor-gc.js b/devtools/server/tests/browser/browser_markers-minor-gc.js new file mode 100644 index 000000000..332764348 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-minor-gc.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "MinorGC" markers when we continue to steadily allocate + * objects. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + // This test runs very slowly on linux32 debug EC2 instances. + requestLongerTimeout(2); + + let doc = yield addTab(MAIN_DOMAIN + "doc_allocations.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, ["MinorGC"]); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === "MinorGC" && m.causeName), + "got some MinorGC markers"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-parse-html.js b/devtools/server/tests/browser/browser_markers-parse-html.js new file mode 100644 index 000000000..bd4f479c0 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-parse-html.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "Parse HTML" markers. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const MARKER_NAME = "Parse HTML"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, MARKER_NAME); + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-styles.js b/devtools/server/tests/browser/browser_markers-styles.js new file mode 100644 index 000000000..a3dffe8b5 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-styles.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get "Styles" markers with correct meta. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const MARKER_NAME = "Styles"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + let markers = yield waitForMarkerType(front, MARKER_NAME, function (markers) { + return markers.some(({restyleHint}) => restyleHint != void 0); + }); + + yield front.stopRecording(rec); + + ok(markers.some(m => m.name === MARKER_NAME), `got some ${MARKER_NAME} markers`); + ok(markers.some(({restyleHint}) => restyleHint != void 0), + "Some markers have a restyleHint."); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_markers-timestamp.js b/devtools/server/tests/browser/browser_markers-timestamp.js new file mode 100644 index 000000000..428499502 --- /dev/null +++ b/devtools/server/tests/browser/browser_markers-timestamp.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get a "TimeStamp" marker. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmConsoleMethod, pmmLoadFrameScripts, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); +const MARKER_NAME = "TimeStamp"; + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + let rec = yield front.startRecording({ withMarkers: true }); + + pmmLoadFrameScripts(gBrowser); + pmmConsoleMethod("timeStamp"); + pmmConsoleMethod("timeStamp", "myLabel"); + + let markers = yield waitForMarkerType(front, MARKER_NAME, markers => markers.length >= 2); + + yield front.stopRecording(rec); + + ok(markers.every(({stack}) => typeof stack === "number"), "All markers have stack references."); + ok(markers.every(({name}) => name === "TimeStamp"), "All markers found are TimeStamp markers"); + ok(markers.length === 2, "found 2 TimeStamp markers"); + ok(markers.every(({start, end}) => typeof start === "number" && start === end), + "All markers have equal start and end times"); + is(markers[0].causeName, void 0, "Unlabeled timestamps have an empty causeName"); + is(markers[1].causeName, "myLabel", "Labeled timestamps have correct causeName"); + + pmmClearFrameScripts(); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_navigateEvents.js b/devtools/server/tests/browser/browser_navigateEvents.js new file mode 100644 index 000000000..f8652f197 --- /dev/null +++ b/devtools/server/tests/browser/browser_navigateEvents.js @@ -0,0 +1,160 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL1 = MAIN_DOMAIN + "navigate-first.html"; +const URL2 = MAIN_DOMAIN + "navigate-second.html"; + +var events = require("sdk/event/core"); +var client; + +SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]}); + +// State machine to check events order +var i = 0; +function assertEvent(event, data) { + let x = 0; + switch (i++) { + case x++: + is(event, "request", "Get first page load"); + is(data, URL1); + break; + case x++: + is(event, "load-new-document", "Ask to load the second page"); + break; + case x++: + is(event, "unload-dialog", "We get the dialog on first page unload"); + break; + case x++: + is(event, "will-navigate", "The very first event is will-navigate on server side"); + is(data.newURI, URL2, "newURI property is correct"); + break; + case x++: + is(event, "request", "RDP is async with messageManager, the request happens after will-navigate"); + is(data, URL2); + break; + case x++: + is(event, "tabNavigated", "After the request, the client receive tabNavigated"); + is(data.state, "start", "state is start"); + is(data.url, URL2, "url property is correct"); + is(data.nativeConsoleAPI, true, "nativeConsoleAPI is correct"); + break; + case x++: + is(event, "DOMContentLoaded"); + is(content.document.readyState, "interactive"); + break; + case x++: + is(event, "load"); + is(content.document.readyState, "complete"); + break; + case x++: + is(event, "navigate", "Then once the second doc is loaded, we get the navigate event"); + is(content.document.readyState, "complete", "navigate is emitted only once the document is fully loaded"); + break; + case x++: + is(event, "tabNavigated", "Finally, the receive the client event"); + is(data.state, "stop", "state is stop"); + is(data.url, URL2, "url property is correct"); + is(data.nativeConsoleAPI, true, "nativeConsoleAPI is correct"); + + // End of test! + cleanup(); + break; + } +} + +function waitForOnBeforeUnloadDialog(browser, callback) { + browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog() { + browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true); + + executeSoon(() => { + let stack = browser.parentNode; + let dialogs = stack.getElementsByTagName("tabmodalprompt"); + let {button0, button1} = dialogs[0].ui; + callback(button0, button1); + }); + }, true); +} + +var httpObserver = function (subject, topic, state) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let url = channel.URI.spec; + // Only listen for our document request, as many other requests can happen + if (url == URL1 || url == URL2) { + assertEvent("request", url); + } +}; +Services.obs.addObserver(httpObserver, "http-on-modify-request", false); + +function onDOMContentLoaded() { + assertEvent("DOMContentLoaded"); +} +function onLoad() { + assertEvent("load"); +} + +function getServerTabActor(callback) { + // Ensure having a minimal server + initDebuggerServer(); + + // Connect to this tab + let transport = DebuggerServer.connectPipe(); + client = new DebuggerClient(transport); + connectDebuggerClient(client).then(form => { + let actorID = form.actor; + client.attachTab(actorID, function (aResponse, aTabClient) { + // !Hack! Retrieve a server side object, the BrowserTabActor instance + let tabActor = DebuggerServer._searchAllConnectionsForActor(actorID); + callback(tabActor); + }); + }); + + client.addListener("tabNavigated", function (aEvent, aPacket) { + assertEvent("tabNavigated", aPacket); + }); +} + +function test() { + // Open a test tab + addTab(URL1).then(function (browser) { + let doc = browser.contentDocument; + getServerTabActor(function (tabActor) { + // In order to listen to internal will-navigate/navigate events + events.on(tabActor, "will-navigate", function (data) { + assertEvent("will-navigate", data); + }); + events.on(tabActor, "navigate", function (data) { + assertEvent("navigate", data); + }); + + // Start listening for page load events + browser.addEventListener("DOMContentLoaded", onDOMContentLoaded, true); + browser.addEventListener("load", onLoad, true); + + // Listen for alert() call being made in navigate-first during unload + waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) { + assertEvent("unload-dialog"); + // accept to quit this page to another + btnLeave.click(); + }); + + // Load another document in this doc to dispatch these events + assertEvent("load-new-document"); + content.location = URL2; + }); + + }); +} + +function cleanup() { + let browser = gBrowser.selectedBrowser; + browser.removeEventListener("DOMContentLoaded", onDOMContentLoaded); + browser.removeEventListener("load", onLoad); + client.close().then(function () { + Services.obs.addObserver(httpObserver, "http-on-modify-request", false); + DebuggerServer.destroy(); + finish(); + }); +} diff --git a/devtools/server/tests/browser/browser_perf-allocation-data.js b/devtools/server/tests/browser/browser_perf-allocation-data.js new file mode 100644 index 000000000..3d4a94dee --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-allocation-data.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we have allocation data coming from the front. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_allocations.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let rec = yield front.startRecording({ withMarkers: true, withAllocations: true, withTicks: true }); + + yield waitUntil(() => rec.getAllocations().frames.length); + yield waitUntil(() => rec.getAllocations().timestamps.length); + yield waitUntil(() => rec.getAllocations().sizes.length); + yield waitUntil(() => rec.getAllocations().sites.length); + + yield front.stopRecording(rec); + + let { frames, timestamps, sizes, sites } = rec.getAllocations(); + + is(timestamps.length, sizes.length, "we have the same amount of timestamps and sizes"); + ok(timestamps.every(time => time > 0 && typeof time === "number"), "all timestamps have numeric values"); + ok(sizes.every(n => n > 0 && typeof n === "number"), "all sizes are positive numbers"); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-profiler-01.js b/devtools/server/tests/browser/browser_perf-profiler-01.js new file mode 100644 index 000000000..36d200f01 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-profiler-01.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the profiler connection front does not activate the built-in + * profiler module if not necessary, and doesn't deactivate it when + * a recording is stopped. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmIsProfilerActive, pmmStopProfiler, pmmLoadFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + pmmLoadFrameScripts(gBrowser); + + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should not have been automatically started."); + + let rec = yield front.startRecording(); + yield front.stopRecording(rec); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active (1)."); + + rec = yield front.startRecording(); + yield front.stopRecording(rec); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active (2)."); + + yield front.destroy(); + yield client.close(); + + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should no longer be active."); + + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-profiler-02.js b/devtools/server/tests/browser/browser_perf-profiler-02.js new file mode 100644 index 000000000..29842cef7 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-profiler-02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the built-in profiler module doesn't deactivate when the toolbox + * is destroyed if there are other consumers using it. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmIsProfilerActive, pmmStopProfiler, pmmLoadFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); + +add_task(function* () { + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let firstFront = PerformanceFront(client, form); + yield firstFront.connect(); + + pmmLoadFrameScripts(gBrowser); + + yield firstFront.startRecording(); + + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let client2 = new DebuggerClient(DebuggerServer.connectPipe()); + let form2 = yield connectDebuggerClient(client2); + let secondFront = PerformanceFront(client2, form2); + yield secondFront.connect(); + pmmLoadFrameScripts(gBrowser); + + yield secondFront.startRecording(); + + // Manually teardown the tabs so we can check profiler status + yield secondFront.destroy(); + yield client2.close(); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active."); + + yield firstFront.destroy(); + yield client.close(); + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should no longer be active."); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-profiler-03.js b/devtools/server/tests/browser/browser_perf-profiler-03.js new file mode 100644 index 000000000..28d87fe45 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-profiler-03.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the built-in profiler module is not reactivated if no other + * consumer was using it over the remote debugger protocol, and ensures + * that the actor will work properly even in such cases (e.g. the Gecko Profiler + * addon was installed and automatically activated the profiler module). + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); +const { pmmIsProfilerActive, pmmStartProfiler, pmmStopProfiler, pmmLoadFrameScripts, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils"); + +add_task(function* () { + // Ensure the profiler is already running when the test starts. + pmmLoadFrameScripts(gBrowser); + let entries = 1000000; + let interval = 1; + let features = ["js"]; + yield pmmStartProfiler({ entries, interval, features }); + + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active."); + + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let firstFront = PerformanceFront(client, form); + yield firstFront.connect(); + + let recording = yield firstFront.startRecording(); + + yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let client2 = new DebuggerClient(DebuggerServer.connectPipe()); + let form2 = yield connectDebuggerClient(client2); + let secondFront = PerformanceFront(client2, form2); + yield secondFront.connect(); + + yield secondFront.destroy(); + yield client2.close(); + ok((yield pmmIsProfilerActive()), + "The built-in profiler module should still be active."); + + yield firstFront.destroy(); + yield client.close(); + ok(!(yield pmmIsProfilerActive()), + "The built-in profiler module should have been automatically stopped."); + + pmmClearFrameScripts(); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-realtime-markers.js b/devtools/server/tests/browser/browser_perf-realtime-markers.js new file mode 100644 index 000000000..b9eab211c --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-realtime-markers.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test functionality of real time markers. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let lastMemoryDelta = 0; + let lastTickDelta = 0; + + let counters = { + markers: [], + memory: [], + ticks: [] + }; + + let deferreds = { + markers: defer(), + memory: defer(), + ticks: defer() + }; + + front.on("timeline-data", handler); + + let rec = yield front.startRecording({ withMarkers: true, withMemory: true, withTicks: true }); + yield Promise.all(Object.keys(deferreds).map(type => deferreds[type].promise)); + yield front.stopRecording(rec); + front.off("timeline-data", handler); + + is(counters.markers.length, 1, "one marker event fired."); + is(counters.memory.length, 3, "three memory events fired."); + is(counters.ticks.length, 3, "three ticks events fired."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); + + function handler(name, data) { + if (name === "markers") { + if (counters.markers.length >= 1) { return; } + ok(data.markers[0].start, "received atleast one marker with `start`"); + ok(data.markers[0].end, "received atleast one marker with `end`"); + ok(data.markers[0].name, "received atleast one marker with `name`"); + + counters.markers.push(data.markers); + } + else if (name === "memory") { + if (counters.memory.length >= 3) { return; } + let { delta, measurement } = data; + is(typeof delta, "number", "received `delta` in memory event"); + ok(delta > lastMemoryDelta, "received `delta` in memory event"); + ok(measurement.total, "received `total` in memory event"); + + counters.memory.push({ delta, measurement }); + lastMemoryDelta = delta; + } + else if (name === "ticks") { + if (counters.ticks.length >= 3) { return; } + let { delta, timestamps } = data; + ok(delta > lastTickDelta, "received `delta` in ticks event"); + + // Timestamps aren't guaranteed to always contain tick events, since + // they're dependent on the refresh driver, which may be blocked. + + counters.ticks.push({ delta, timestamps }); + lastTickDelta = delta; + } + else if (name === "frames") { + // Nothing to do here. + } + else { + ok(false, `Received unknown event: ${name}`); + } + + if (name === "markers" && counters[name].length === 1 || + name === "memory" && counters[name].length === 3 || + name === "ticks" && counters[name].length === 3) { + deferreds[name].resolve(); + } + } +}); diff --git a/devtools/server/tests/browser/browser_perf-recording-actor-01.js b/devtools/server/tests/browser/browser_perf-recording-actor-01.js new file mode 100644 index 000000000..683493121 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-recording-actor-01.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the state of a recording rec from start to finish for recording, + * completed, and rec data. + */ + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let rec = yield front.startRecording({ withMarkers: true, withTicks: true, withMemory: true }); + ok(rec.isRecording(), "RecordingModel is recording when created"); + yield busyWait(100); + yield waitUntil(() => rec.getMemory().length); + ok(true, "RecordingModel populates memory while recording"); + yield waitUntil(() => rec.getTicks().length); + ok(true, "RecordingModel populates ticks while recording"); + yield waitUntil(() => rec.getMarkers().length); + ok(true, "RecordingModel populates markers while recording"); + + ok(!rec.isCompleted(), "RecordingModel is not completed when still recording"); + + let stopping = once(front, "recording-stopping"); + let stopped = once(front, "recording-stopped"); + front.stopRecording(rec); + + yield stopping; + ok(!rec.isRecording(), "on 'recording-stopping', model is no longer recording"); + // This handler should be called BEFORE "recording-stopped" is called, as + // there is some delay, but in the event where "recording-stopped" finishes + // before we check here, ensure that we're atleast in the right state + if (rec.getProfile()) { + ok(rec.isCompleted(), "recording is completed once it has profile data"); + } else { + ok(!rec.isCompleted(), "recording is not yet completed on 'recording-stopping'"); + ok(rec.isFinalizing(), "recording is considered finalizing between 'recording-stopping' and 'recording-stopped'"); + } + + yield stopped; + ok(!rec.isRecording(), "on 'recording-stopped', model is still no longer recording"); + ok(rec.isCompleted(), "on 'recording-stopped', model is considered 'complete'"); + + checkSystemInfo(rec, "Host"); + checkSystemInfo(rec, "Client"); + + // Export and import a rec, and ensure it has the correct state. + let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8)); + yield rec.exportRecording(file); + + let importedModel = yield front.importRecording(file); + + ok(importedModel.isCompleted(), "All imported recordings should be completed"); + ok(!importedModel.isRecording(), "All imported recordings should not be recording"); + ok(importedModel.isImported(), "All imported recordings should be considerd imported"); + + checkSystemInfo(importedModel, "Host"); + checkSystemInfo(importedModel, "Client"); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function checkSystemInfo(recording, type) { + let data = recording[`get${type}SystemInfo`](); + for (let field of ["appid", "apptype", "vendor", "name", "version"]) { + ok(data[field], `get${type}SystemInfo() has ${field} property`); + } +} diff --git a/devtools/server/tests/browser/browser_perf-recording-actor-02.js b/devtools/server/tests/browser/browser_perf-recording-actor-02.js new file mode 100644 index 000000000..8337ad2ef --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-recording-actor-02.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that buffer status is correctly updated in recording models. + */ + +var BUFFER_SIZE = 20000; +var config = { bufferSize: BUFFER_SIZE }; + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + yield front.setProfilerStatusInterval(10); + let model = yield front.startRecording(config); + let stats = yield once(front, "profiler-status"); + is(stats.totalSize, BUFFER_SIZE, `profiler-status event has totalSize: ${stats.totalSize}`); + ok(stats.position < BUFFER_SIZE, `profiler-status event has position: ${stats.position}`); + ok(stats.generation >= 0, `profiler-status event has generation: ${stats.generation}`); + ok(stats.isActive, "profiler-status event is isActive"); + is(typeof stats.currentTime, "number", "profiler-status event has currentTime"); + + // Halt once more for a buffer status to ensure we're beyond 0 + yield once(front, "profiler-status"); + + let lastBufferStatus = 0; + let checkCount = 0; + while (lastBufferStatus < 1) { + let currentBufferStatus = front.getBufferUsageForRecording(model); + ok(currentBufferStatus > lastBufferStatus, `buffer is more filled than before: ${currentBufferStatus} > ${lastBufferStatus}`); + lastBufferStatus = currentBufferStatus; + checkCount++; + yield once(front, "profiler-status"); + } + + ok(checkCount >= 1, "atleast 1 event were fired until the buffer was filled"); + is(lastBufferStatus, 1, "buffer usage cannot surpass 100%"); + yield front.stopRecording(model); + + is(front.getBufferUsageForRecording(model), null, "buffer usage should be null when no longer recording."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-samples-01.js b/devtools/server/tests/browser/browser_perf-samples-01.js new file mode 100644 index 000000000..f8f4bf393 --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-samples-01.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the retrieved profiler data samples are correctly filtered and + * normalized before passed to consumers. + */ + +const WAIT_TIME = 1000; // ms + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + // Perform the first recording... + + let firstRecording = yield front.startRecording(); + let firstRecordingStartTime = firstRecording._startTime; + info("Started profiling at: " + firstRecordingStartTime); + + busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity + + yield front.stopRecording(firstRecording); + + ok(firstRecording.getDuration() >= WAIT_TIME, + "The first recording duration is correct."); + + // Perform the second recording... + + let secondRecording = yield front.startRecording(); + let secondRecordingStartTime = secondRecording._startTime; + info("Started profiling at: " + secondRecordingStartTime); + + busyWait(WAIT_TIME); // allow the profiler module to sample more cpu activity + + yield front.stopRecording(secondRecording); + let secondRecordingProfile = secondRecording.getProfile(); + let secondRecordingSamples = secondRecordingProfile.threads[0].samples.data; + + ok(secondRecording.getDuration() >= WAIT_TIME, + "The second recording duration is correct."); + + const TIME_SLOT = secondRecordingProfile.threads[0].samples.schema.time; + ok(secondRecordingSamples[0][TIME_SLOT] < secondRecordingStartTime, + "The second recorded sample times were normalized."); + ok(secondRecordingSamples[0][TIME_SLOT] > 0, + "The second recorded sample times were normalized correctly."); + ok(!secondRecordingSamples.find(e => e[TIME_SLOT] + secondRecordingStartTime <= firstRecording.getDuration()), + "There should be no samples from the first recording in the second one, " + + "even though the total number of frames did not overflow."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_perf-samples-02.js b/devtools/server/tests/browser/browser_perf-samples-02.js new file mode 100644 index 000000000..c4d51230d --- /dev/null +++ b/devtools/server/tests/browser/browser_perf-samples-02.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the retrieved profiler data samples always have a (root) node. + * If this ever changes, the |ThreadNode.prototype.insert| function in + * devtools/client/performance/modules/logic/tree-model.js will have to be changed. + */ + +const WAIT_TIME = 1000; // ms + +const { PerformanceFront } = require("devtools/shared/fronts/performance"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "doc_perf.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = PerformanceFront(client, form); + yield front.connect(); + + let rec = yield front.startRecording(); + busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity + + yield front.stopRecording(rec); + let profile = rec.getProfile(); + let sampleCount = 0; + + for (let thread of profile.threads) { + info("Checking thread: " + thread.name); + + for (let sample of thread.samples.data) { + sampleCount++; + + let stack = getInflatedStackLocations(thread, sample); + if (stack[0] != "(root)") { + ok(false, "The sample " + stack.toSource() + " doesn't have a root node."); + } + } + } + + ok(sampleCount > 0, + "At least some samples have been iterated over, checking for root nodes."); + + yield front.destroy(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +/** + * Inflate a particular sample's stack and return an array of strings. + */ +function getInflatedStackLocations(thread, sample) { + let stackTable = thread.stackTable; + let frameTable = thread.frameTable; + let stringTable = thread.stringTable; + let SAMPLE_STACK_SLOT = thread.samples.schema.stack; + let STACK_PREFIX_SLOT = stackTable.schema.prefix; + let STACK_FRAME_SLOT = stackTable.schema.frame; + let FRAME_LOCATION_SLOT = frameTable.schema.location; + + // Build the stack from the raw data and accumulate the locations in + // an array. + let stackIndex = sample[SAMPLE_STACK_SLOT]; + let locations = []; + while (stackIndex !== null) { + let stackEntry = stackTable.data[stackIndex]; + let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]]; + locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]); + stackIndex = stackEntry[STACK_PREFIX_SLOT]; + } + + // The profiler tree is inverted, so reverse the array. + return locations.reverse(); +} diff --git a/devtools/server/tests/browser/browser_register_actor.js b/devtools/server/tests/browser/browser_register_actor.js new file mode 100644 index 000000000..73ee0cedc --- /dev/null +++ b/devtools/server/tests/browser/browser_register_actor.js @@ -0,0 +1,76 @@ +var gClient; + +function test() { + waitForExplicitFinish(); + var {ActorRegistryFront} = require("devtools/shared/fronts/actor-registry"); + var actorURL = "chrome://mochitests/content/chrome/devtools/server/tests/mochitest/hello-actor.js"; + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + gClient = new DebuggerClient(DebuggerServer.connectPipe()); + gClient.connect() + .then(() => gClient.listTabs()) + .then(aResponse => { + + var options = { + prefix: "helloActor", + constructor: "HelloActor", + type: { tab: true } + }; + + var registry = ActorRegistryFront(gClient, aResponse); + registry.registerActor(actorURL, options).then(actorFront => { + gClient.listTabs(response => { + var tab = response.tabs[response.selected]; + ok(!!tab.helloActor, "Hello actor must exist"); + + // Make sure actor's state is maintained across listTabs requests. + checkActorState(tab.helloActor, () => { + + // Clean up + actorFront.unregister().then(() => { + gClient.close().then(() => { + DebuggerServer.destroy(); + gClient = null; + finish(); + }); + }); + }); + }); + }); + }); +} + +function checkActorState(helloActor, callback) { + getCount(helloActor, response => { + ok(!response.error, "No error"); + is(response.count, 1, "The counter must be valid"); + + getCount(helloActor, response => { + ok(!response.error, "No error"); + is(response.count, 2, "The counter must be valid"); + + gClient.listTabs(response => { + var tab = response.tabs[response.selected]; + is(tab.helloActor, helloActor, "Hello actor must be valid"); + + getCount(helloActor, response => { + ok(!response.error, "No error"); + is(response.count, 3, "The counter must be valid"); + + callback(); + }); + }); + }); + }); +} + +function getCount(actor, callback) { + gClient.request({ + to: actor, + type: "count" + }, callback); +} diff --git a/devtools/server/tests/browser/browser_storage_dynamic_windows.js b/devtools/server/tests/browser/browser_storage_dynamic_windows.js new file mode 100644 index 000000000..440c91222 --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_dynamic_windows.js @@ -0,0 +1,294 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {StorageFront} = require("devtools/shared/fronts/storage"); +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this); + +const beforeReload = { + cookies: { + "test1.example.org": ["c1", "cs2", "c3", "uc1"], + "sectest1.example.org": ["uc1", "cs2"] + }, + localStorage: { + "http://test1.example.org": ["ls1", "ls2"], + "http://sectest1.example.org": ["iframe-u-ls1"] + }, + sessionStorage: { + "http://test1.example.org": ["ss1"], + "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"] + }, + indexedDB: { + "http://test1.example.org": [ + JSON.stringify(["idb1", "obj1"]), + JSON.stringify(["idb1", "obj2"]), + JSON.stringify(["idb2", "obj3"]), + ], + "http://sectest1.example.org": [] + } +}; + +function* testStores(data, front) { + testWindowsBeforeReload(data); + + // FIXME: Bug 1183581 - browser_storage_dynamic_windows.js IsSafeToRunScript + // errors when testing reload in E10S mode + // yield testReload(front); + yield testAddIframe(front); + yield testRemoveIframe(front); +} + +function testWindowsBeforeReload(data) { + for (let storageType in beforeReload) { + ok(data[storageType], storageType + " storage actor is present"); + is(Object.keys(data[storageType].hosts).length, + Object.keys(beforeReload[storageType]).length, + "Number of hosts for " + storageType + "match"); + for (let host in beforeReload[storageType]) { + ok(data[storageType].hosts[host], "Host " + host + " is present"); + } + } +} + +function markOutMatched(toBeEmptied, data, deleted) { + if (!Object.keys(toBeEmptied).length) { + info("Object empty"); + return; + } + ok(Object.keys(data).length, + "At least one storage type should be present"); + for (let storageType in toBeEmptied) { + if (!data[storageType]) { + continue; + } + info("Testing for " + storageType); + for (let host in data[storageType]) { + ok(toBeEmptied[storageType][host], "Host " + host + " found"); + if (!deleted) { + for (let item of data[storageType][host]) { + let index = toBeEmptied[storageType][host].indexOf(item); + ok(index > -1, "Item found - " + item); + if (index > -1) { + toBeEmptied[storageType][host].splice(index, 1); + } + } + if (!toBeEmptied[storageType][host].length) { + delete toBeEmptied[storageType][host]; + } + } else { + delete toBeEmptied[storageType][host]; + } + } + if (!Object.keys(toBeEmptied[storageType]).length) { + delete toBeEmptied[storageType]; + } + } +} + +// function testReload(front) { +// info("Testing if reload works properly"); + +// let shouldBeEmptyFirst = Cu.cloneInto(beforeReload, {}); +// let shouldBeEmptyLast = Cu.cloneInto(beforeReload, {}); +// return new Promise(resolve => { + +// let onStoresUpdate = data => { +// info("in stores update of testReload"); +// // This might be second time stores update is happening, in which case, +// // data.deleted will be null. +// // OR.. This might be the first time on a super slow machine where both +// // data.deleted and data.added is missing in the first update. +// if (data.deleted) { +// markOutMatched(shouldBeEmptyFirst, data.deleted, true); +// } + +// if (!Object.keys(shouldBeEmptyFirst).length) { +// info("shouldBeEmptyFirst is empty now"); +// } + +// // stores-update call might not have data.added for the first time on +// // slow machines, in which case, data.added will be null +// if (data.added) { +// markOutMatched(shouldBeEmptyLast, data.added); +// } + +// if (!Object.keys(shouldBeEmptyLast).length) { +// info("Everything to be received is received."); +// endTestReloaded(); +// } +// }; + +// let endTestReloaded = () => { +// front.off("stores-update", onStoresUpdate); +// resolve(); +// }; + +// front.on("stores-update", onStoresUpdate); + +// content.location.reload(); +// }); +// } + +function testAddIframe(front) { + info("Testing if new iframe addition works properly"); + return new Promise(resolve => { + let shouldBeEmpty = { + localStorage: { + "https://sectest1.example.org": ["iframe-s-ls1"] + }, + sessionStorage: { + "https://sectest1.example.org": ["iframe-s-ss1"] + }, + cookies: { + "sectest1.example.org": ["sc1"] + }, + indexedDB: { + // empty because indexed db creation happens after the page load, so at + // the time of window-ready, there was no indexed db present. + "https://sectest1.example.org": [] + }, + Cache: { + "https://sectest1.example.org":[] + } + }; + + let onStoresUpdate = data => { + info("checking if the hosts list is correct for this iframe addition"); + + markOutMatched(shouldBeEmpty, data.added); + + ok(!data.changed || !data.changed.cookies || + !data.changed.cookies["https://sectest1.example.org"], + "Nothing got changed for cookies"); + ok(!data.changed || !data.changed.localStorage || + !data.changed.localStorage["https://sectest1.example.org"], + "Nothing got changed for local storage"); + ok(!data.changed || !data.changed.sessionStorage || + !data.changed.sessionStorage["https://sectest1.example.org"], + "Nothing got changed for session storage"); + ok(!data.changed || !data.changed.indexedDB || + !data.changed.indexedDB["https://sectest1.example.org"], + "Nothing got changed for indexed db"); + + ok(!data.deleted || !data.deleted.cookies || + !data.deleted.cookies["https://sectest1.example.org"], + "Nothing got deleted for cookies"); + ok(!data.deleted || !data.deleted.localStorage || + !data.deleted.localStorage["https://sectest1.example.org"], + "Nothing got deleted for local storage"); + ok(!data.deleted || !data.deleted.sessionStorage || + !data.deleted.sessionStorage["https://sectest1.example.org"], + "Nothing got deleted for session storage"); + ok(!data.deleted || !data.deleted.indexedDB || + !data.deleted.indexedDB["https://sectest1.example.org"], + "Nothing got deleted for indexed db"); + + if (!Object.keys(shouldBeEmpty).length) { + info("Everything to be received is received."); + endTestReloaded(); + } + }; + + let endTestReloaded = () => { + front.off("stores-update", onStoresUpdate); + resolve(); + }; + + front.on("stores-update", onStoresUpdate); + + let iframe = content.document.createElement("iframe"); + iframe.src = ALT_DOMAIN_SECURED + "storage-secured-iframe.html"; + content.document.querySelector("body").appendChild(iframe); + }); +} + +function testRemoveIframe(front) { + info("Testing if iframe removal works properly"); + return new Promise(resolve => { + let shouldBeEmpty = { + localStorage: { + "http://sectest1.example.org": [] + }, + sessionStorage: { + "http://sectest1.example.org": [] + }, + Cache: { + "http://sectest1.example.org": [] + }, + indexedDB: { + "http://sectest1.example.org": [] + } + }; + + let onStoresUpdate = data => { + info("checking if the hosts list is correct for this iframe deletion"); + + markOutMatched(shouldBeEmpty, data.deleted, true); + + ok(!data.deleted.cookies || !data.deleted.cookies["sectest1.example.org"], + "Nothing got deleted for Cookies as " + + "the same hostname is still present"); + + ok(!data.changed || !data.changed.cookies || + !data.changed.cookies["http://sectest1.example.org"], + "Nothing got changed for cookies"); + ok(!data.changed || !data.changed.localStorage || + !data.changed.localStorage["http://sectest1.example.org"], + "Nothing got changed for local storage"); + ok(!data.changed || !data.changed.sessionStorage || + !data.changed.sessionStorage["http://sectest1.example.org"], + "Nothing got changed for session storage"); + + ok(!data.added || !data.added.cookies || + !data.added.cookies["http://sectest1.example.org"], + "Nothing got added for cookies"); + ok(!data.added || !data.added.localStorage || + !data.added.localStorage["http://sectest1.example.org"], + "Nothing got added for local storage"); + ok(!data.added || !data.added.sessionStorage || + !data.added.sessionStorage["http://sectest1.example.org"], + "Nothing got added for session storage"); + + if (!Object.keys(shouldBeEmpty).length) { + info("Everything to be received is received."); + endTestReloaded(); + } + }; + + let endTestReloaded = () => { + front.off("stores-update", onStoresUpdate); + resolve(); + }; + + front.on("stores-update", onStoresUpdate); + + for (let iframe of content.document.querySelectorAll("iframe")) { + if (iframe.src.startsWith("http:")) { + iframe.remove(); + break; + } + } + }); +} + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-dynamic-windows.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let data = yield front.listStores(); + yield testStores(data, front); + + yield clearStorage(); + + // Forcing GC/CC to get rid of docshells and windows created by this test. + forceCollections(); + yield client.close(); + forceCollections(); + DebuggerServer.destroy(); + forceCollections(); +}); diff --git a/devtools/server/tests/browser/browser_storage_listings.js b/devtools/server/tests/browser/browser_storage_listings.js new file mode 100644 index 000000000..4ff3c3fc1 --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_listings.js @@ -0,0 +1,610 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {StorageFront} = require("devtools/shared/fronts/storage"); +Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js", this); + +const storeMap = { + cookies: { + "test1.example.org": [ + { + name: "c1", + value: "foobar", + expires: 2000000000000, + path: "/browser", + host: "test1.example.org", + isDomain: false, + isSecure: false, + }, + { + name: "cs2", + value: "sessionCookie", + path: "/", + host: ".example.org", + expires: 0, + isDomain: true, + isSecure: false, + }, + { + name: "c3", + value: "foobar-2", + expires: 2000000001000, + path: "/", + host: "test1.example.org", + isDomain: false, + isSecure: false, + }, + { + name: "uc1", + value: "foobar", + host: ".example.org", + path: "/", + expires: 0, + isDomain: true, + isSecure: true, + } + ], + "sectest1.example.org": [ + { + name: "uc1", + value: "foobar", + host: ".example.org", + path: "/", + expires: 0, + isDomain: true, + isSecure: true, + }, + { + name: "cs2", + value: "sessionCookie", + path: "/", + host: ".example.org", + expires: 0, + isDomain: true, + isSecure: false, + }, + { + name: "sc1", + value: "foobar", + path: "/browser/devtools/server/tests/browser/", + host: "sectest1.example.org", + expires: 0, + isDomain: false, + isSecure: false, + } + ] + }, + localStorage: { + "http://test1.example.org": [ + { + name: "ls1", + value: "foobar" + }, + { + name: "ls2", + value: "foobar-2" + } + ], + "http://sectest1.example.org": [ + { + name: "iframe-u-ls1", + value: "foobar" + } + ], + "https://sectest1.example.org": [ + { + name: "iframe-s-ls1", + value: "foobar" + } + ] + }, + sessionStorage: { + "http://test1.example.org": [ + { + name: "ss1", + value: "foobar-3" + } + ], + "http://sectest1.example.org": [ + { + name: "iframe-u-ss1", + value: "foobar1" + }, + { + name: "iframe-u-ss2", + value: "foobar2" + } + ], + "https://sectest1.example.org": [ + { + name: "iframe-s-ss1", + value: "foobar-2" + } + ] + } +}; + +const IDBValues = { + listStoresResponse: { + "http://test1.example.org": [ + ["idb1", "obj1"], ["idb1", "obj2"], ["idb2", "obj3"] + ], + "http://sectest1.example.org": [ + ], + "https://sectest1.example.org": [ + ["idb-s1", "obj-s1"], ["idb-s2", "obj-s2"] + ] + }, + dbDetails : { + "http://test1.example.org": [ + { + db: "idb1", + origin: "http://test1.example.org", + version: 1, + objectStores: 2 + }, + { + db: "idb2", + origin: "http://test1.example.org", + version: 1, + objectStores: 1 + }, + ], + "http://sectest1.example.org": [ + ], + "https://sectest1.example.org": [ + { + db: "idb-s1", + origin: "https://sectest1.example.org", + version: 1, + objectStores: 1 + }, + { + db: "idb-s2", + origin: "https://sectest1.example.org", + version: 1, + objectStores: 1 + }, + ] + }, + objectStoreDetails: { + "http://test1.example.org": { + idb1: [ + { + objectStore: "obj1", + keyPath: "id", + autoIncrement: false, + indexes: [ + { + name: "name", + keyPath: "name", + "unique": false, + multiEntry: false, + }, + { + name: "email", + keyPath: "email", + "unique": true, + multiEntry: false, + }, + ] + }, + { + objectStore: "obj2", + keyPath: "id2", + autoIncrement: false, + indexes: [] + } + ], + idb2: [ + { + objectStore: "obj3", + keyPath: "id3", + autoIncrement: false, + indexes: [ + { + name: "name2", + keyPath: "name2", + "unique": true, + multiEntry: false, + } + ] + }, + ] + }, + "http://sectest1.example.org" : {}, + "https://sectest1.example.org": { + "idb-s1": [ + { + objectStore: "obj-s1", + keyPath: "id", + autoIncrement: false, + indexes: [] + }, + ], + "idb-s2": [ + { + objectStore: "obj-s2", + keyPath: "id3", + autoIncrement: true, + indexes: [ + { + name: "name2", + keyPath: "name2", + "unique": true, + multiEntry: false, + } + ] + }, + ] + } + + }, + entries: { + "http://test1.example.org": { + "idb1#obj1": [ + { + name: 1, + value: { + id: 1, + name: "foo", + email: "foo@bar.com", + } + }, + { + name: 2, + value: { + id: 2, + name: "foo2", + email: "foo2@bar.com", + } + }, + { + name: 3, + value: { + id: 3, + name: "foo2", + email: "foo3@bar.com", + } + } + ], + "idb1#obj2": [ + { + name: 1, + value: { + id2: 1, + name: "foo", + email: "foo@bar.com", + extra: "baz" + } + } + ], + "idb2#obj3": [] + }, + "http://sectest1.example.org" : {}, + "https://sectest1.example.org": { + "idb-s1#obj-s1": [ + { + name: 6, + value: { + id: 6, + name: "foo", + email: "foo@bar.com", + } + }, + { + name: 7, + value: { + id: 7, + name: "foo2", + email: "foo2@bar.com", + } + } + ], + "idb-s2#obj-s2": [ + { + name: 13, + value: { + id2: 13, + name2: "foo", + email: "foo@bar.com", + } + } + ] + } + } +}; + +function finishTests(client) { + + let closeConnection = () => { + + }; +} + +function* testStores(data) { + ok(data.cookies, "Cookies storage actor is present"); + ok(data.localStorage, "Local Storage storage actor is present"); + ok(data.sessionStorage, "Session Storage storage actor is present"); + ok(data.indexedDB, "Indexed DB storage actor is present"); + yield testCookies(data.cookies); + yield testLocalStorage(data.localStorage); + yield testSessionStorage(data.sessionStorage); + yield testIndexedDB(data.indexedDB); +} + +function testCookies(cookiesActor) { + is(Object.keys(cookiesActor.hosts).length, 2, "Correct number of host entries for cookies"); + return testCookiesObjects(0, cookiesActor.hosts, cookiesActor); +} + +var testCookiesObjects = Task.async(function* (index, hosts, cookiesActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + let cookiesLength = 0; + for (let secureCookie of storeMap.cookies[host]) { + if (secureCookie.isSecure) { + ++cookiesLength; + } + } + // Any secure cookies did not get stored in the database. + is(data.total, storeMap.cookies[host].length - cookiesLength, + "Number of cookies in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of storeMap.cookies[host]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found cookie " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + is(item.expires, toMatch.expires, "The expiry time matches."); + is(item.path, toMatch.path, "The path matches."); + is(item.host, toMatch.host, "The host matches."); + is(item.isSecure, toMatch.isSecure, "The isSecure value matches."); + is(item.isDomain, toMatch.isDomain, "The isDomain value matches."); + break; + } + } + ok(found, "cookie " + item.name + " should exist in response"); + } + }; + + ok(!!storeMap.cookies[host], "Host is present in the list : " + host); + matchItems(yield cookiesActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testCookiesObjects(++index, hosts, cookiesActor); +}); + +function testLocalStorage(localStorageActor) { + is(Object.keys(localStorageActor.hosts).length, 3, + "Correct number of host entries for local storage"); + return testLocalStorageObjects(0, localStorageActor.hosts, localStorageActor); +} + +var testLocalStorageObjects = Task.async(function* (index, hosts, localStorageActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, storeMap.localStorage[host].length, + "Number of local storage items in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of storeMap.localStorage[host]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found local storage item " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + break; + } + } + ok(found, "local storage item " + item.name + " should exist in response"); + } + }; + + ok(!!storeMap.localStorage[host], "Host is present in the list : " + host); + matchItems(yield localStorageActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testLocalStorageObjects(++index, hosts, localStorageActor); +}); + +function testSessionStorage(sessionStorageActor) { + is(Object.keys(sessionStorageActor.hosts).length, 3, + "Correct number of host entries for session storage"); + return testSessionStorageObjects(0, sessionStorageActor.hosts, + sessionStorageActor); +} + +var testSessionStorageObjects = Task.async(function* (index, hosts, sessionStorageActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, storeMap.sessionStorage[host].length, + "Number of session storage items in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of storeMap.sessionStorage[host]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found session storage item " + item.name + " in response"); + is(item.value.str, toMatch.value, "The value matches."); + break; + } + } + ok(found, "session storage item " + item.name + " should exist in response"); + } + }; + + ok(!!storeMap.sessionStorage[host], "Host is present in the list : " + host); + matchItems(yield sessionStorageActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testSessionStorageObjects(++index, hosts, sessionStorageActor); +}); + +var testIndexedDB = Task.async(function* (indexedDBActor) { + is(Object.keys(indexedDBActor.hosts).length, 3, + "Correct number of host entries for indexed db"); + + for (let host in indexedDBActor.hosts) { + for (let item of indexedDBActor.hosts[host]) { + let parsedItem = JSON.parse(item); + let found = false; + for (let toMatch of IDBValues.listStoresResponse[host]) { + if (toMatch[0] == parsedItem[0] && toMatch[1] == parsedItem[1]) { + found = true; + break; + } + } + ok(found, item + " should exist in list stores response"); + } + } + + yield testIndexedDBs(0, indexedDBActor.hosts, indexedDBActor); + yield testObjectStores(0, indexedDBActor.hosts, indexedDBActor); + yield testIDBEntries(0, indexedDBActor.hosts, indexedDBActor); +}); + +var testIndexedDBs = Task.async(function* (index, hosts, indexedDBActor) { + let host = Object.keys(hosts)[index]; + let matchItems = data => { + is(data.total, IDBValues.dbDetails[host].length, + "Number of indexed db in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of IDBValues.dbDetails[host]) { + if (item.db == toMatch.db) { + found = true; + ok(true, "Found indexed db " + item.db + " in response"); + is(item.origin, toMatch.origin, "The origin matches."); + is(item.version, toMatch.version, "The version matches."); + is(item.objectStores, toMatch.objectStores, + "The numebr of object stores matches."); + break; + } + } + ok(found, "indexed db " + item.name + " should exist in response"); + } + }; + + ok(!!IDBValues.dbDetails[host], "Host is present in the list : " + host); + matchItems(yield indexedDBActor.getStoreObjects(host)); + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testIndexedDBs(++index, hosts, indexedDBActor); +}); + +var testObjectStores = Task.async(function* (index, hosts, indexedDBActor) { + let host = Object.keys(hosts)[index]; + let matchItems = (data, db) => { + is(data.total, IDBValues.objectStoreDetails[host][db].length, + "Number of object stores in host " + host + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of IDBValues.objectStoreDetails[host][db]) { + if (item.objectStore == toMatch.objectStore) { + found = true; + ok(true, "Found object store " + item.objectStore + " in response"); + is(item.keyPath, toMatch.keyPath, "The keyPath matches."); + is(item.autoIncrement, toMatch.autoIncrement, "The autoIncrement matches."); + item.indexes = JSON.parse(item.indexes); + is(item.indexes.length, toMatch.indexes.length, "Number of indexes match"); + for (let index of item.indexes) { + let indexFound = false; + for (let toMatchIndex of toMatch.indexes) { + if (toMatchIndex.name == index.name) { + indexFound = true; + ok(true, "Found index " + index.name); + is(index.keyPath, toMatchIndex.keyPath, + "The keyPath of index matches."); + is(index.unique, toMatchIndex.unique, "The unique matches"); + is(index.multiEntry, toMatchIndex.multiEntry, + "The multiEntry matches"); + break; + } + } + ok(indexFound, "Index " + index + " should exist in response"); + } + break; + } + } + ok(found, "indexed db " + item.name + " should exist in response"); + } + }; + + ok(!!IDBValues.objectStoreDetails[host], "Host is present in the list : " + host); + for (let name of hosts[host]) { + let objName = JSON.parse(name).slice(0, 1); + matchItems(( + yield indexedDBActor.getStoreObjects(host, [JSON.stringify(objName)]) + ), objName[0]); + } + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testObjectStores(++index, hosts, indexedDBActor); +}); + +var testIDBEntries = Task.async(function* (index, hosts, indexedDBActor) { + let host = Object.keys(hosts)[index]; + let matchItems = (data, obj) => { + is(data.total, IDBValues.entries[host][obj].length, + "Number of items in object store " + obj + " matches"); + for (let item of data.data) { + let found = false; + for (let toMatch of IDBValues.entries[host][obj]) { + if (item.name == toMatch.name) { + found = true; + ok(true, "Found indexed db item " + item.name + " in response"); + let value = JSON.parse(item.value.str); + is(Object.keys(value).length, Object.keys(toMatch.value).length, + "Number of entries in the value matches"); + for (let key in value) { + is(value[key], toMatch.value[key], + "value of " + key + " value key matches"); + } + break; + } + } + ok(found, "indexed db item " + item.name + " should exist in response"); + } + }; + + ok(!!IDBValues.entries[host], "Host is present in the list : " + host); + for (let name of hosts[host]) { + let parsed = JSON.parse(name); + matchItems(( + yield indexedDBActor.getStoreObjects(host, [name]) + ), parsed[0] + "#" + parsed[1]); + } + if (index == Object.keys(hosts).length - 1) { + return; + } + yield testObjectStores(++index, hosts, indexedDBActor); +}); + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let data = yield front.listStores(); + yield testStores(data); + + yield clearStorage(); + + // Forcing GC/CC to get rid of docshells and windows created by this test. + forceCollections(); + yield client.close(); + forceCollections(); + DebuggerServer.destroy(); + forceCollections(); +}); diff --git a/devtools/server/tests/browser/browser_storage_updates.js b/devtools/server/tests/browser/browser_storage_updates.js new file mode 100644 index 000000000..28b2e509f --- /dev/null +++ b/devtools/server/tests/browser/browser_storage_updates.js @@ -0,0 +1,304 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {StorageFront} = require("devtools/shared/fronts/storage"); +const beforeReload = { + cookies: ["test1.example.org", "sectest1.example.org"], + localStorage: ["http://test1.example.org", "http://sectest1.example.org"], + sessionStorage: ["http://test1.example.org", "http://sectest1.example.org"], +}; + +const TESTS = [ + // index 0 + { + action: function (win) { + info('win.addCookie("c1", "foobar1")'); + win.addCookie("c1", "foobar1"); + + info('win.addCookie("c2", "foobar2")'); + win.addCookie("c2", "foobar2"); + + info('win.localStorage.setItem("l1", "foobar1")'); + win.localStorage.setItem("l1", "foobar1"); + }, + expected: { + added: { + cookies: { + "test1.example.org": ["c1", "c2"] + }, + localStorage: { + "http://test1.example.org": ["l1"] + } + } + } + }, + + // index 1 + { + action: function (win) { + info('win.addCookie("c1", "new_foobar1")'); + win.addCookie("c1", "new_foobar1"); + + info('win.localStorage.setItem("l2", "foobar2")'); + win.localStorage.setItem("l2", "foobar2"); + }, + expected: { + changed: { + cookies: { + "test1.example.org": ["c1"] + } + }, + added: { + localStorage: { + "http://test1.example.org": ["l2"] + } + } + } + }, + + // index 2 + { + action: function (win) { + info('win.removeCookie("c2")'); + win.removeCookie("c2"); + + info('win.localStorage.removeItem("l1")'); + win.localStorage.removeItem("l1"); + + info('win.localStorage.setItem("l3", "foobar3")'); + win.localStorage.setItem("l3", "foobar3"); + }, + expected: { + deleted: { + cookies: { + "test1.example.org": ["c2"] + }, + localStorage: { + "http://test1.example.org": ["l1"] + } + }, + added: { + localStorage: { + "http://test1.example.org": ["l3"] + } + } + } + }, + + // index 3 + { + action: function (win) { + info('win.removeCookie("c1")'); + win.removeCookie("c1"); + + info('win.addCookie("c3", "foobar3")'); + win.addCookie("c3", "foobar3"); + + info('win.localStorage.removeItem("l2")'); + win.localStorage.removeItem("l2"); + + info('win.sessionStorage.setItem("s1", "foobar1")'); + win.sessionStorage.setItem("s1", "foobar1"); + + info('win.sessionStorage.setItem("s2", "foobar2")'); + win.sessionStorage.setItem("s2", "foobar2"); + + info('win.localStorage.setItem("l3", "new_foobar3")'); + win.localStorage.setItem("l3", "new_foobar3"); + }, + expected: { + added: { + cookies: { + "test1.example.org": ["c3"] + }, + sessionStorage: { + "http://test1.example.org": ["s1", "s2"] + } + }, + changed: { + localStorage: { + "http://test1.example.org": ["l3"] + } + }, + deleted: { + cookies: { + "test1.example.org": ["c1"] + }, + localStorage: { + "http://test1.example.org": ["l2"] + } + } + } + }, + + // index 4 + { + action: function (win) { + info('win.sessionStorage.removeItem("s1")'); + win.sessionStorage.removeItem("s1"); + }, + expected: { + deleted: { + sessionStorage: { + "http://test1.example.org": ["s1"] + } + } + } + }, + + // index 5 + { + action: function (win) { + info("win.clearCookies()"); + win.clearCookies(); + }, + expected: { + deleted: { + cookies: { + "test1.example.org": ["c3"] + } + } + } + } +]; + +function markOutMatched(toBeEmptied, data) { + if (!Object.keys(toBeEmptied).length) { + info("Object empty"); + return; + } + ok(Object.keys(data).length, "At least one storage type should be present"); + + for (let storageType in toBeEmptied) { + if (!data[storageType]) { + continue; + } + info("Testing for " + storageType); + for (let host in data[storageType]) { + ok(toBeEmptied[storageType][host], "Host " + host + " found"); + + for (let item of data[storageType][host]) { + let index = toBeEmptied[storageType][host].indexOf(item); + ok(index > -1, "Item found - " + item); + if (index > -1) { + toBeEmptied[storageType][host].splice(index, 1); + } + } + if (!toBeEmptied[storageType][host].length) { + delete toBeEmptied[storageType][host]; + } + } + if (!Object.keys(toBeEmptied[storageType]).length) { + delete toBeEmptied[storageType]; + } + } +} + +function onStoresUpdate(expected, {added, changed, deleted}, index) { + info("inside stores update for index " + index); + + // Here, added, changed and deleted might be null even if they are required as + // per expected. This is fine as they might come in the next stores-update + // call or have already come in the previous one. + if (added) { + info("matching added object for index " + index); + markOutMatched(expected.added, added); + } + if (changed) { + info("matching changed object for index " + index); + markOutMatched(expected.changed, changed); + } + if (deleted) { + info("matching deleted object for index " + index); + markOutMatched(expected.deleted, deleted); + } + + if ((!expected.added || !Object.keys(expected.added).length) && + (!expected.changed || !Object.keys(expected.changed).length) && + (!expected.deleted || !Object.keys(expected.deleted).length)) { + info("Everything expected has been received for index " + index); + } else { + info("Still some updates pending for index " + index); + } +} + +function runTest({action, expected}, front, win, index) { + return new Promise(resolve => { + front.once("stores-update", function (addedChangedDeleted) { + onStoresUpdate(expected, addedChangedDeleted, index); + resolve(); + }); + + info("Running test at index " + index); + action(win); + }); +} + +function* testClearLocalAndSessionStores(front, win) { + return new Promise(resolve => { + // We need to wait until we have received stores-cleared for both local and + // session storage. + let localStorage = false; + let sessionStorage = false; + + front.on("stores-cleared", function onStoresCleared(data) { + storesCleared(data); + + if (data.localStorage) { + localStorage = true; + } + if (data.sessionStorage) { + sessionStorage = true; + } + if (localStorage && sessionStorage) { + front.off("stores-cleared", onStoresCleared); + resolve(); + } + }); + + win.clearLocalAndSessionStores(); + }); +} + +function storesCleared(data) { + if (data.sessionStorage || data.localStorage) { + let hosts = data.sessionStorage || data.localStorage; + info("Stores cleared required for session storage"); + is(hosts.length, 1, "number of hosts is 1"); + is(hosts[0], "http://test1.example.org", + "host matches for " + Object.keys(data)[0]); + } else { + ok(false, "Stores cleared should only be for local and session storage"); + } +} + +function* finishTests(client) { + yield client.close(); + DebuggerServer.destroy(); + finish(); +} + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "storage-updates.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = StorageFront(client, form); + let win = doc.defaultView.wrappedJSObject; + + yield front.listStores(); + + for (let i = 0; i < TESTS.length; i++) { + let test = TESTS[i]; + yield runTest(test, front, win, i); + } + + yield testClearLocalAndSessionStores(front, win); + yield finishTests(client); +}); diff --git a/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js new file mode 100644 index 000000000..f7bb7057e --- /dev/null +++ b/devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js @@ -0,0 +1,40 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that StyleSheetActor.getText handles empty text correctly. + +const {StyleSheetsFront} = require("devtools/shared/fronts/stylesheets"); + +const CONTENT = "<style>body { background-color: #f0c; }</style>"; +const TEST_URI = "data:text/html;charset=utf-8," + encodeURIComponent(CONTENT); + +add_task(function* () { + yield addTab(TEST_URI); + + info("Initialising the debugger server and client."); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + info("Attaching to the active tab."); + yield client.attachTab(form.actor); + + let front = StyleSheetsFront(client, form); + ok(front, "The StyleSheetsFront was created."); + + let sheets = yield front.getStyleSheets(); + ok(sheets, "getStyleSheets() succeeded"); + is(sheets.length, 1, + "getStyleSheets() returned the correct number of sheets"); + + let sheet = sheets[0]; + yield sheet.update("", false); + let longStr = yield sheet.getText(); + let source = yield longStr.string(); + is(source, "", "text is empty"); + + yield client.close(); +}); diff --git a/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js b/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js new file mode 100644 index 000000000..c382b6ce8 --- /dev/null +++ b/devtools/server/tests/browser/browser_stylesheets_nested-iframes.js @@ -0,0 +1,38 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that StyleSheetsActor.getStyleSheets() works if an iframe does not have +// a content document. + +const {StyleSheetsFront} = require("devtools/shared/fronts/stylesheets"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "stylesheets-nested-iframes.html"); + let doc = browser.contentDocument; + + info("Initialising the debugger server and client."); + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + + info("Attaching to the active tab."); + yield client.attachTab(form.actor); + + let front = StyleSheetsFront(client, form); + ok(front, "The StyleSheetsFront was created."); + + let sheets = yield front.getStyleSheets(); + ok(sheets, "getStyleSheets() succeeded even with documentless iframes."); + + // Bug 285395 limits the number of nested iframes to 10. There's one sheet per + // frame so we should get 10 sheets. However, the limit might change in the + // future so it's better not to rely on the limit. Asserting > 2 ensures that + // the test page is actually loading nested iframes and this test is doing + // something sensible (if we got this far, the test has served its purpose). + ok(sheets.length > 2, sheets.length + " sheets found (expected 3 or more)."); + + yield client.close(); +}); diff --git a/devtools/server/tests/browser/browser_timeline.js b/devtools/server/tests/browser/browser_timeline.js new file mode 100644 index 000000000..1e5793447 --- /dev/null +++ b/devtools/server/tests/browser/browser_timeline.js @@ -0,0 +1,63 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the timeline front's start/stop/isRecording methods work in a +// simple use case, and that markers events are sent when operations occur. +// Note that this test isn't concerned with which markers are actually recorded, +// just that markers are recorded at all. +// Trying to check marker types here may lead to intermittents, see bug 1066474. + +const {TimelineFront} = require("devtools/shared/fronts/timeline"); + +add_task(function* () { + let browser = yield addTab("data:text/html;charset=utf-8,mop"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + + ok(front, "The TimelineFront was created"); + + let isActive = yield front.isRecording(); + ok(!isActive, "The TimelineFront is not initially recording"); + + info("Flush any pending reflows"); + let forceSyncReflow = doc.body.innerHeight; + + info("Start recording"); + yield front.start({ withMarkers: true }); + + isActive = yield front.isRecording(); + ok(isActive, "The TimelineFront is now recording"); + + info("Change some style on the page to cause style/reflow/paint"); + let onMarkers = once(front, "markers"); + doc.body.style.padding = "10px"; + let markers = yield onMarkers; + + ok(true, "The markers event was fired"); + ok(markers.length > 0, "Markers were returned"); + + info("Flush pending reflows again"); + forceSyncReflow = doc.body.innerHeight; + + info("Change some style on the page to cause style/paint"); + onMarkers = once(front, "markers"); + doc.body.style.backgroundColor = "red"; + markers = yield onMarkers; + + ok(markers.length > 0, "markers were returned"); + + yield front.stop(); + + isActive = yield front.isRecording(); + ok(!isActive, "Not recording after stop()"); + + yield client.close(); + gBrowser.removeCurrentTab(); +}); diff --git a/devtools/server/tests/browser/browser_timeline_actors.js b/devtools/server/tests/browser/browser_timeline_actors.js new file mode 100644 index 000000000..a902775fa --- /dev/null +++ b/devtools/server/tests/browser/browser_timeline_actors.js @@ -0,0 +1,69 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the timeline can also record data from the memory and framerate +// actors, emitted as events in tadem with the markers. + +const {TimelineFront} = require("devtools/shared/fronts/timeline"); + +add_task(function* () { + let browser = yield addTab("data:text/html;charset=utf-8,mop"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + + info("Start timeline marker recording"); + yield front.start({ withMemory: true, withTicks: true }); + + let updatedMemory = 0; + let updatedTicks = 0; + + front.on("memory", (delta, measurement) => { + ok(delta > 0, "The delta should be a timestamp."); + ok(measurement, "The measurement should not be null."); + ok(measurement.total > 0, "There should be a 'total' value in the measurement."); + info("Received 'memory' event at " + delta + " with " + measurement.toSource()); + updatedMemory++; + }); + + front.on("ticks", (delta, ticks) => { + ok(delta > 0, "The delta should be a timestamp."); + ok(ticks, "The ticks should not be null."); + info("Received 'ticks' event with " + ticks.toSource()); + updatedTicks++; + }); + + ok((yield waitUntil(() => updatedMemory > 1)), + "Some memory measurements were emitted."); + ok((yield waitUntil(() => updatedTicks > 1)), + "Some refresh driver ticks were emitted."); + + info("Stop timeline marker recording"); + yield front.stop(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => + setTimeout(function () { + waitUntil(predicate).then(() => resolve(true)); + }, interval)); +} diff --git a/devtools/server/tests/browser/browser_timeline_iframes.js b/devtools/server/tests/browser/browser_timeline_iframes.js new file mode 100644 index 000000000..60728873f --- /dev/null +++ b/devtools/server/tests/browser/browser_timeline_iframes.js @@ -0,0 +1,41 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the timeline front receives markers events for operations that occur in +// iframes. + +const {TimelineFront} = require("devtools/shared/fronts/timeline"); + +add_task(function* () { + let browser = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html"); + let doc = browser.contentDocument; + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let front = TimelineFront(client, form); + + info("Start timeline marker recording"); + yield front.start({ withMarkers: true }); + + // Check that we get markers for a few iterations of the timer that runs in + // the child frame. + for (let i = 0; i < 3; i++) { + yield wait(300); // That's the time the child frame waits before changing styles. + let markers = yield once(front, "markers"); + ok(markers.length, "Markers were received for operations in the child frame"); + } + + info("Stop timeline marker recording"); + yield front.stop(); + yield client.close(); + gBrowser.removeCurrentTab(); +}); + +function wait(ms) { + return new Promise(resolve => + setTimeout(resolve, ms)); +} diff --git a/devtools/server/tests/browser/director-script-target.html b/devtools/server/tests/browser/director-script-target.html new file mode 100644 index 000000000..0b0b56d64 --- /dev/null +++ b/devtools/server/tests/browser/director-script-target.html @@ -0,0 +1,15 @@ +<html> + <head> + <script> + // change the eval function to ensure the window object in the debug-script is correctly wrapped + window.eval = function () { + return "unsecure-eval-called"; + }; + + var globalAccessibleVar = "global-value"; + </script> + </head> + <body> + <h1>debug script target</h1> + </body> +</html> diff --git a/devtools/server/tests/browser/doc_allocations.html b/devtools/server/tests/browser/doc_allocations.html new file mode 100644 index 000000000..314c40cac --- /dev/null +++ b/devtools/server/tests/browser/doc_allocations.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<script> +window.allocs = []; +window.onload = function() { + function allocator() { + for (var i = 0; i < 1000; i++) { + window.allocs.push(new Object); + } + } + + window.setInterval(allocator, 1); +}; +</script> +</pre> +</body> +</html> diff --git a/devtools/server/tests/browser/doc_force_cc.html b/devtools/server/tests/browser/doc_force_cc.html new file mode 100644 index 000000000..d5868bd6b --- /dev/null +++ b/devtools/server/tests/browser/doc_force_cc.html @@ -0,0 +1,29 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Performance tool + cycle collection test page</title> + </head> + + <body> + <script type="text/javascript"> + window.test = function () { + document.body.expando1 = { cycle: document.body }; + SpecialPowers.Cu.forceCC(); + + document.body.expando2 = { cycle: document.body }; + SpecialPowers.Cu.forceCC(); + + document.body.expando3 = { cycle: document.body }; + SpecialPowers.Cu.forceCC(); + + setTimeout(window.test, 100); + }; + test(); + </script> + </body> + +</html> diff --git a/devtools/server/tests/browser/doc_force_gc.html b/devtools/server/tests/browser/doc_force_gc.html new file mode 100644 index 000000000..f8b617533 --- /dev/null +++ b/devtools/server/tests/browser/doc_force_gc.html @@ -0,0 +1,27 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Performance tool + garbage collection test page</title> + </head> + + <body> + <script type="text/javascript"> + var x = 1; + window.test = function () { + SpecialPowers.Cu.forceGC(); + document.body.style.borderTop = x + "px solid red"; + x = 1^x; + document.body.innerHeight; // flush pending reflows + + // Prevent this script from being garbage collected. + setTimeout(window.test, 100); + }; + test(); + </script> + </body> + +</html> diff --git a/devtools/server/tests/browser/doc_innerHTML.html b/devtools/server/tests/browser/doc_innerHTML.html new file mode 100644 index 000000000..f5ce72de2 --- /dev/null +++ b/devtools/server/tests/browser/doc_innerHTML.html @@ -0,0 +1,21 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Performance tool + innerHTML test page</title> + </head> + + <body> + <script type="text/javascript"> + "use strict"; + window.test = function () { + document.body.innerHTML = "<h1>LOL</h1>"; + }; + setInterval(window.test, 100); + </script> + </body> + +</html> diff --git a/devtools/server/tests/browser/doc_perf.html b/devtools/server/tests/browser/doc_perf.html new file mode 100644 index 000000000..1da36328b --- /dev/null +++ b/devtools/server/tests/browser/doc_perf.html @@ -0,0 +1,25 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!doctype html> + +<html> + <head> + <meta charset="utf-8"/> + <title>Performance test page</title> + </head> + + <body> + <script type="text/javascript"> + var x = 1; + function test() { + document.body.style.borderTop = x + "px solid red"; + x = 1^x; + document.body.innerHeight; // flush pending reflows + } + + // Prevent this script from being garbage collected. + window.setInterval(test, 1); + </script> + </body> + +</html> diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js new file mode 100644 index 000000000..1e7f09d95 --- /dev/null +++ b/devtools/server/tests/browser/head.js @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +const {console} = Cu.import("resource://gre/modules/Console.jsm", {}); +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {DebuggerClient} = require("devtools/shared/client/main"); +const {DebuggerServer} = require("devtools/server/main"); +const {defer} = require("promise"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const Services = require("Services"); + +const PATH = "browser/devtools/server/tests/browser/"; +const MAIN_DOMAIN = "http://test1.example.org/" + PATH; +const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; +const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; + +// All tests are asynchronous. +waitForExplicitFinish(); + +/** + * Add a new test tab in the browser and load the given url. + * @param {String} url The url to be loaded in the new tab + * @return a promise that resolves to the new browser that the document + * is loaded in. Note that we cannot return the document + * directly, since this would be a CPOW in the e10s case, + * and Promises cannot be resolved with CPOWs (see bug 1233497). + */ +var addTab = Task.async(function* (url) { + info(`Adding a new tab with URL: ${url}`); + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + yield BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + info(`Tab added and URL ${url} loaded`); + + return tab.linkedBrowser; +}); + +function* initAnimationsFrontForUrl(url) { + const {AnimationsFront} = require("devtools/shared/fronts/animation"); + const {InspectorFront} = require("devtools/shared/fronts/inspector"); + + yield addTab(url); + + initDebuggerServer(); + let client = new DebuggerClient(DebuggerServer.connectPipe()); + let form = yield connectDebuggerClient(client); + let inspector = InspectorFront(client, form); + let walker = yield inspector.getWalker(); + let animations = AnimationsFront(client, form); + + return {inspector, walker, animations, client}; +} + +function initDebuggerServer() { + try { + // Sometimes debugger server does not get destroyed correctly by previous + // tests. + DebuggerServer.destroy(); + } catch (e) { + info(`DebuggerServer destroy error: ${e}\n${e.stack}`); + } + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); +} + +/** + * Connect a debugger client. + * @param {DebuggerClient} + * @return {Promise} Resolves to the selected tabActor form when the client is + * connected. + */ +function connectDebuggerClient(client) { + return client.connect() + .then(() => client.listTabs()) + .then(tabs => { + return tabs.tabs[tabs.selected]; + }); +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture = false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + return new Promise(resolve => { + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + info("Got event: '" + eventName + "' on " + target + "."); + target[remove](eventName, onEvent, useCapture); + resolve(...aArgs); + }, useCapture); + break; + } + } + }); +} + +/** + * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and + * windows. + */ +function forceCollections() { + Cu.forceGC(); + Cu.forceCC(); + Cu.forceShrinkingGC(); +} + +/** + * Get a mock tabActor from a given window. + * This is sometimes useful to test actors or classes that use the tabActor in + * isolation. + * @param {DOMWindow} win + * @return {Object} + */ +function getMockTabActor(win) { + return { + window: win, + isRootActor: true + }; +} + +registerCleanupFunction(function tearDown() { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +function idleWait(time) { + return DevToolsUtils.waitForTime(time); +} + +function busyWait(time) { + let start = Date.now(); + let stack; + while (Date.now() - start < time) { stack = Components.stack; } +} + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return Promise.resolve(true); + } + return new Promise(resolve => { + setTimeout(function () { + waitUntil(predicate).then(() => resolve(true)); + }, interval); + }); +} + +function waitForMarkerType(front, types, predicate, + unpackFun = (name, data) => data.markers, + eventName = "timeline-data") +{ + types = [].concat(types); + predicate = predicate || function () { return true; }; + let filteredMarkers = []; + let { promise, resolve } = defer(); + + info("Waiting for markers of type: " + types); + + function handler(name, data) { + if (typeof name === "string" && name !== "markers") { + return; + } + + let markers = unpackFun(name, data); + info("Got markers: " + JSON.stringify(markers, null, 2)); + + filteredMarkers = filteredMarkers.concat(markers.filter(m => types.indexOf(m.name) !== -1)); + + if (types.every(t => filteredMarkers.some(m => m.name === t)) && predicate(filteredMarkers)) { + front.off(eventName, handler); + resolve(filteredMarkers); + } + } + front.on(eventName, handler); + + return promise; +} diff --git a/devtools/server/tests/browser/navigate-first.html b/devtools/server/tests/browser/navigate-first.html new file mode 100644 index 000000000..829372427 --- /dev/null +++ b/devtools/server/tests/browser/navigate-first.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +First +<script> + +window.onbeforeunload=function(e){ + e.returnValue="?"; +}; +</script> +</body> +</html> diff --git a/devtools/server/tests/browser/navigate-second.html b/devtools/server/tests/browser/navigate-second.html new file mode 100644 index 000000000..4b30fe465 --- /dev/null +++ b/devtools/server/tests/browser/navigate-second.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +Second +</body> +</html> diff --git a/devtools/server/tests/browser/storage-dynamic-windows.html b/devtools/server/tests/browser/storage-dynamic-windows.html new file mode 100644 index 000000000..67aa35d67 --- /dev/null +++ b/devtools/server/tests/browser/storage-dynamic-windows.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 965872 - Storage inspector actor with cookies, local storage and session storage. +--> +<head> + <meta charset="utf-8"> + <title>Storage inspector test for listing hosts and storages</title> +</head> +<body> +<iframe src="http://sectest1.example.org/browser/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe> +<script type="application/javascript;version=1.7"> +"use strict"; +const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1]; +const cookieExpiresTime1 = 2000000000000; +const cookieExpiresTime2 = 2000000001000; +// Setting up some cookies to eat. +document.cookie = "c1=foobar; expires=" + + new Date(cookieExpiresTime1).toGMTString() + "; path=/browser"; +document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname; +document.cookie = "c3=foobar-2; expires=" + + new Date(cookieExpiresTime2).toGMTString() + "; path=/"; +// ... and some local storage items .. +localStorage.setItem("ls1", "foobar"); +localStorage.setItem("ls2", "foobar-2"); +// ... and finally some session storage items too +sessionStorage.setItem("ss1", "foobar-3"); + +let idbGenerator = function*() { + let request = indexedDB.open("idb1", 1); + request.onerror = function() { + throw new Error("error opening db connection"); + }; + let db = yield new Promise(done => { + request.onupgradeneeded = event => { + let db = event.target.result; + let store1 = db.createObjectStore("obj1", { keyPath: "id" }); + store1.createIndex("name", "name", { unique: false }); + store1.createIndex("email", "email", { unique: true }); + let store2 = db.createObjectStore("obj2", { keyPath: "id2" }); + store1.transaction.oncomplete = () => { + done(db); + }; + }; + }); + + // Prevents AbortError + yield new Promise(done => { + request.onsuccess = done; + }); + + let transaction = db.transaction(["obj1", "obj2"], "readwrite"); + let store1 = transaction.objectStore("obj1"); + let store2 = transaction.objectStore("obj2"); + store1.add({id: 1, name: "foo", email: "foo@bar.com"}); + store1.add({id: 2, name: "foo2", email: "foo2@bar.com"}); + store1.add({id: 3, name: "foo2", email: "foo3@bar.com"}); + store2.add({ + id2: 1, + name: "foo", + email: "foo@bar.com", + extra: "baz" + }); + // Prevents AbortError during close() + yield new Promise(success => { + transaction.oncomplete = success; + }); + + db.close(); + + request = indexedDB.open("idb2", 1); + let db2 = yield new Promise(done => { + request.onupgradeneeded = event => { + let db2 = event.target.result; + let store3 = db2.createObjectStore("obj3", { keyPath: "id3" }); + store3.createIndex("name2", "name2", { unique: true }); + store3.transaction.oncomplete = () => { + done(db2); + } + }; + }); + // Prevents AbortError during close() + yield new Promise(done => { + request.onsuccess = done; + }); + db2.close(); + + console.log("added cookies and stuff from main page"); +}; + +function deleteDB(dbName) { + return new Promise(resolve => { + dump("removing database " + dbName + " from " + document.location + "\n"); + indexedDB.deleteDatabase(dbName).onsuccess = resolve; + }); +} + +window.setup = function*() { + yield idbGenerator(); +}; + +window.clear = function*() { + document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + document.cookie = "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + document.cookie = "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + + localStorage.clear(); + + yield deleteDB("idb1"); + yield deleteDB("idb2"); + + dump("removed cookies, localStorage and indexedDB data from " + + document.location + "\n"); +}; +</script> +</body> +</html> diff --git a/devtools/server/tests/browser/storage-helpers.js b/devtools/server/tests/browser/storage-helpers.js new file mode 100644 index 000000000..1c4f37705 --- /dev/null +++ b/devtools/server/tests/browser/storage-helpers.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This generator function opens the given url in a new tab, then sets up the + * page by waiting for all cookies, indexedDB items etc. to be created. + * + * @param url {String} The url to be opened in the new tab + * + * @return {Promise} A promise that resolves after storage inspector is ready + */ +function* openTabAndSetupStorage(url) { + let content = yield addTab(url); + + // Setup the async storages in main window and for all its iframes + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + /** + * Get all windows including frames recursively. + * + * @param {Window} [baseWindow] + * The base window at which to start looking for child windows + * (optional). + * @return {Set} + * A set of windows. + */ + function getAllWindows(baseWindow) { + let windows = new Set(); + + let _getAllWindows = function (win) { + windows.add(win.wrappedJSObject); + + for (let i = 0; i < win.length; i++) { + _getAllWindows(win[i]); + } + }; + _getAllWindows(baseWindow); + + return windows; + } + + let windows = getAllWindows(content); + for (let win of windows) { + if (win.setup) { + yield win.setup(); + } + } + }); +} + +function* clearStorage() { + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + /** + * Get all windows including frames recursively. + * + * @param {Window} [baseWindow] + * The base window at which to start looking for child windows + * (optional). + * @return {Set} + * A set of windows. + */ + function getAllWindows(baseWindow) { + let windows = new Set(); + + let _getAllWindows = function (win) { + windows.add(win.wrappedJSObject); + + for (let i = 0; i < win.length; i++) { + _getAllWindows(win[i]); + } + }; + _getAllWindows(baseWindow); + + return windows; + } + + let windows = getAllWindows(content); + for (let win of windows) { + if (win.clear) { + yield win.clear(); + } + } + }); +} diff --git a/devtools/server/tests/browser/storage-listings.html b/devtools/server/tests/browser/storage-listings.html new file mode 100644 index 000000000..c3b7ef3c8 --- /dev/null +++ b/devtools/server/tests/browser/storage-listings.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 965872 - Storage inspector actor with cookies, local storage and session storage. +--> +<head> + <meta charset="utf-8"> + <title>Storage inspector test for listing hosts and storages</title> +</head> +<body> +<iframe src="http://sectest1.example.org/browser/devtools/server/tests/browser/storage-unsecured-iframe.html"></iframe> +<iframe src="https://sectest1.example.org:443/browser/devtools/server/tests/browser/storage-secured-iframe.html"></iframe> +<script type="application/javascript;version=1.7"> +"use strict"; +const partialHostname = location.hostname.match(/^[^.]+(\..*)$/)[1]; +const cookieExpiresTime1 = 2000000000000; +const cookieExpiresTime2 = 2000000001000; +// Setting up some cookies to eat. +document.cookie = "c1=foobar; expires=" + + new Date(cookieExpiresTime1).toGMTString() + "; path=/browser"; +document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname; +document.cookie = "c3=foobar-2; secure=true; expires=" + + new Date(cookieExpiresTime2).toGMTString() + "; path=/"; +// ... and some local storage items .. +localStorage.setItem("ls1", "foobar"); +localStorage.setItem("ls2", "foobar-2"); +// ... and finally some session storage items too +sessionStorage.setItem("ss1", "foobar-3"); +console.log("added cookies and stuff from main page"); + +let idbGenerator = function*() { + let request = indexedDB.open("idb1", 1); + request.onerror = function() { + throw new Error("error opening db connection"); + }; + let db = yield new Promise(done => { + request.onupgradeneeded = event => { + let db = event.target.result; + let store1 = db.createObjectStore("obj1", { keyPath: "id" }); + store1.createIndex("name", "name", { unique: false }); + store1.createIndex("email", "email", { unique: true }); + let store2 = db.createObjectStore("obj2", { keyPath: "id2" }); + store1.transaction.oncomplete = () => { + done(db); + }; + }; + }); + + // Prevents AbortError + yield new Promise(done => { + request.onsuccess = done; + }); + + let transaction = db.transaction(["obj1", "obj2"], "readwrite"); + let store1 = transaction.objectStore("obj1"); + let store2 = transaction.objectStore("obj2"); + store1.add({id: 1, name: "foo", email: "foo@bar.com"}); + store1.add({id: 2, name: "foo2", email: "foo2@bar.com"}); + store1.add({id: 3, name: "foo2", email: "foo3@bar.com"}); + store2.add({ + id2: 1, + name: "foo", + email: "foo@bar.com", + extra: "baz" + }); + // Prevents AbortError during close() + yield new Promise(success => { + transaction.oncomplete = success; + }); + + db.close(); + + request = indexedDB.open("idb2", 1); + let db2 = yield new Promise(done => { + request.onupgradeneeded = event => { + let db2 = event.target.result; + let store3 = db2.createObjectStore("obj3", { keyPath: "id3" }); + store3.createIndex("name2", "name2", { unique: true }); + store3.transaction.oncomplete = () => { + done(db2); + } + }; + }); + // Prevents AbortError during close() + yield new Promise(done => { + request.onsuccess = done; + }); + db2.close(); + + dump("added cookies and stuff from main page\n"); +}; + +function deleteDB(dbName) { + return new Promise(resolve => { + dump("removing database " + dbName + " from " + document.location + "\n"); + indexedDB.deleteDatabase(dbName).onsuccess = resolve; + }); +} + +window.setup = function*() { + yield idbGenerator(); +}; + +window.clear = function*() { + document.cookie = "c1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/browser"; + document.cookie = + "c3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure=true"; + document.cookie = + "cs2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=" + + partialHostname; + + localStorage.clear(); + sessionStorage.clear(); + + yield deleteDB("idb1"); + yield deleteDB("idb2"); + + dump("removed cookies, localStorage, sessionStorage and indexedDB data " + + "from " + document.location + "\n"); +}; +</script> +</body> +</html> diff --git a/devtools/server/tests/browser/storage-secured-iframe.html b/devtools/server/tests/browser/storage-secured-iframe.html new file mode 100644 index 000000000..860b20aab --- /dev/null +++ b/devtools/server/tests/browser/storage-secured-iframe.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<!-- +Iframe for testing multiple host detetion in storage actor +--> +<head> + <meta charset="utf-8"> +</head> +<body> +<script type="application/javascript;version=1.7"> + +document.cookie = "sc1=foobar;"; +localStorage.setItem("iframe-s-ls1", "foobar"); +sessionStorage.setItem("iframe-s-ss1", "foobar-2"); + +let idbGenerator = function*() { + let request = indexedDB.open("idb-s1", 1); + request.onerror = function() { + throw new Error("error opening db connection"); + }; + let db = yield new Promise(done => { + request.onupgradeneeded = event => { + let db = event.target.result; + let store1 = db.createObjectStore("obj-s1", { keyPath: "id" }); + store1.transaction.oncomplete = () => { + done(db); + }; + }; + }); + yield new Promise(done => { + request.onsuccess = done; + }); + + let transaction = db.transaction(["obj-s1"], "readwrite"); + let store1 = transaction.objectStore("obj-s1"); + store1.add({id: 6, name: "foo", email: "foo@bar.com"}); + store1.add({id: 7, name: "foo2", email: "foo2@bar.com"}); + yield new Promise(success => { + transaction.oncomplete = success; + }); + + db.close(); + + request = indexedDB.open("idb-s2", 1); + let db2 = yield new Promise(done => { + request.onupgradeneeded = event => { + let db2 = event.target.result; + let store3 = + db2.createObjectStore("obj-s2", { keyPath: "id3", autoIncrement: true }); + store3.createIndex("name2", "name2", { unique: true }); + store3.transaction.oncomplete = () => { + done(db2); + }; + }; + }); + yield new Promise(done => { + request.onsuccess = done; + }); + + transaction = db2.transaction(["obj-s2"], "readwrite"); + let store3 = transaction.objectStore("obj-s2"); + store3.add({id3: 16, name2: "foo", email: "foo@bar.com"}); + yield new Promise(success => { + transaction.oncomplete = success; + }); + + db2.close(); + dump("added cookies and stuff from secured iframe\n"); +} + +function deleteDB(dbName) { + return new Promise(resolve => { + dump("removing database " + dbName + " from " + document.location + "\n"); + indexedDB.deleteDatabase(dbName).onsuccess = resolve; + }); +} + +window.setup = function*() { + yield idbGenerator(); +}; + +window.clear = function*() { + document.cookie = "sc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + + localStorage.clear(); + + yield deleteDB("idb-s1"); + yield deleteDB("idb-s2"); + + console.log("removed cookies and stuff from secured iframe"); +} +</script> +</body> +</html> diff --git a/devtools/server/tests/browser/storage-unsecured-iframe.html b/devtools/server/tests/browser/storage-unsecured-iframe.html new file mode 100644 index 000000000..d339fb7f2 --- /dev/null +++ b/devtools/server/tests/browser/storage-unsecured-iframe.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<!-- +Iframe for testing multiple host detetion in storage actor +--> +<head> + <meta charset="utf-8"> +</head> +<body> +<script> + +document.cookie = "uc1=foobar; domain=.example.org; path=/; secure=true"; +localStorage.setItem("iframe-u-ls1", "foobar"); +sessionStorage.setItem("iframe-u-ss1", "foobar1"); +sessionStorage.setItem("iframe-u-ss2", "foobar2"); +console.log("added cookies and stuff from unsecured iframe"); + +window.clear = function*() { + document.cookie = "uc1=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + localStorage.clear(); + sessionStorage.clear(); + console.log("removed cookies and stuff from unsecured iframe"); +} +</script> +</body> +</html> diff --git a/devtools/server/tests/browser/storage-updates.html b/devtools/server/tests/browser/storage-updates.html new file mode 100644 index 000000000..6ffaf4316 --- /dev/null +++ b/devtools/server/tests/browser/storage-updates.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 965872 - Storage inspector actor with cookies, local storage and session storage. +--> +<head> + <meta charset="utf-8"> + <title>Storage inspector blank html for tests</title> +</head> +<body> +<script type="application/javascript;version=1.7"> +"use strict"; +window.addCookie = function(name, value, path, domain, expires, secure) { + let cookieString = name + "=" + value + ";"; + if (path) { + cookieString += "path=" + path + ";"; + } + if (domain) { + cookieString += "domain=" + domain + ";"; + } + if (expires) { + cookieString += "expires=" + expires + ";"; + } + if (secure) { + cookieString += "secure=true;"; + } + document.cookie = cookieString; +}; + +window.removeCookie = function(name) { + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; +}; + +window.clearLocalAndSessionStores = function() { + localStorage.clear(); + sessionStorage.clear(); +}; + +window.clearCookies = function() { + let cookies = document.cookie; + for (let cookie of cookies.split(";")) { + removeCookie(cookie.split("=")[0]); + } +}; +</script> +</body> +</html> diff --git a/devtools/server/tests/browser/stylesheets-nested-iframes.html b/devtools/server/tests/browser/stylesheets-nested-iframes.html new file mode 100644 index 000000000..7ee775323 --- /dev/null +++ b/devtools/server/tests/browser/stylesheets-nested-iframes.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>StyleSheetsActor iframe test</title> + <style> + p { + padding: 1em; + } + </style> +</head> +<body> + <p>A test page with nested iframes</p> + <iframe></iframe> + <script type="application/javascript;version=1.8"> + let iframe = document.querySelector("iframe"); + let i = parseInt(location.href.split("?")[1]) || 1; + + // The frame can't have the same src URL as any of its ancestors. + // This will not infinitely recurse because a frame won't get a content + // document once it's nested deeply enough. + iframe.src = location.href.split("?")[0] + "?" + (++i); + </script> +</body> +</html> diff --git a/devtools/server/tests/browser/timeline-iframe-child.html b/devtools/server/tests/browser/timeline-iframe-child.html new file mode 100644 index 000000000..5385c6485 --- /dev/null +++ b/devtools/server/tests/browser/timeline-iframe-child.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Timeline iframe test - child frame</title> +</head> +<body> + <h1>Child frame</h1> + <script> + var h1 = document.querySelector("h1"); + setInterval(function() { + h1.style.backgroundColor = "rgb(" + ((Math.random()*255)|0) + "," + + ((Math.random()*255)|0) + "," + + ((Math.random()*255)|0) +")"; + h1.style.width = ((Math.random()*500)|0) + "px"; + }, 300); + </script> +</body> +</html> diff --git a/devtools/server/tests/browser/timeline-iframe-parent.html b/devtools/server/tests/browser/timeline-iframe-parent.html new file mode 100644 index 000000000..b94ba4259 --- /dev/null +++ b/devtools/server/tests/browser/timeline-iframe-parent.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Timeline iframe test - parent frame</title> +</head> +<body> + <h1>Parent frame</h1> + <iframe src="timeline-iframe-child.html"></iframe> +</body> +</html> |