summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/tests/browser')
-rw-r--r--devtools/server/tests/browser/.eslintrc.js6
-rw-r--r--devtools/server/tests/browser/animation.html170
-rw-r--r--devtools/server/tests/browser/browser.ini97
-rw-r--r--devtools/server/tests/browser/browser_animation_emitMutations.js62
-rw-r--r--devtools/server/tests/browser/browser_animation_getFrames.js32
-rw-r--r--devtools/server/tests/browser/browser_animation_getMultipleStates.js55
-rw-r--r--devtools/server/tests/browser/browser_animation_getPlayers.js63
-rw-r--r--devtools/server/tests/browser/browser_animation_getProperties.js36
-rw-r--r--devtools/server/tests/browser/browser_animation_getStateAfterFinished.js55
-rw-r--r--devtools/server/tests/browser/browser_animation_getSubTreeAnimations.js38
-rw-r--r--devtools/server/tests/browser/browser_animation_keepFinished.js54
-rw-r--r--devtools/server/tests/browser/browser_animation_playPauseIframe.js51
-rw-r--r--devtools/server/tests/browser/browser_animation_playPauseSeveral.js92
-rw-r--r--devtools/server/tests/browser/browser_animation_playerState.js123
-rw-r--r--devtools/server/tests/browser/browser_animation_reconstructState.js38
-rw-r--r--devtools/server/tests/browser/browser_animation_refreshTransitions.js77
-rw-r--r--devtools/server/tests/browser/browser_animation_setCurrentTime.js74
-rw-r--r--devtools/server/tests/browser/browser_animation_setPlaybackRate.js51
-rw-r--r--devtools/server/tests/browser/browser_animation_simple.js35
-rw-r--r--devtools/server/tests/browser/browser_animation_updatedState.js55
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_01.js90
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_02.js48
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_03.js102
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_04.js98
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_05.js112
-rw-r--r--devtools/server/tests/browser/browser_canvasframe_helper_06.js100
-rw-r--r--devtools/server/tests/browser/browser_directorscript_actors.js159
-rw-r--r--devtools/server/tests/browser/browser_directorscript_actors_error_events.js132
-rw-r--r--devtools/server/tests/browser/browser_directorscript_actors_exports.js87
-rw-r--r--devtools/server/tests/browser/browser_markers-cycle-collection.js33
-rw-r--r--devtools/server/tests/browser/browser_markers-docloading-01.js37
-rw-r--r--devtools/server/tests/browser/browser_markers-docloading-02.js35
-rw-r--r--devtools/server/tests/browser/browser_markers-docloading-03.js39
-rw-r--r--devtools/server/tests/browser/browser_markers-gc.js50
-rw-r--r--devtools/server/tests/browser/browser_markers-minor-gc.js32
-rw-r--r--devtools/server/tests/browser/browser_markers-parse-html.js29
-rw-r--r--devtools/server/tests/browser/browser_markers-styles.js34
-rw-r--r--devtools/server/tests/browser/browser_markers-timestamp.js43
-rw-r--r--devtools/server/tests/browser/browser_navigateEvents.js160
-rw-r--r--devtools/server/tests/browser/browser_perf-allocation-data.js38
-rw-r--r--devtools/server/tests/browser/browser_perf-profiler-01.js45
-rw-r--r--devtools/server/tests/browser/browser_perf-profiler-02.js46
-rw-r--r--devtools/server/tests/browser/browser_perf-profiler-03.js54
-rw-r--r--devtools/server/tests/browser/browser_perf-realtime-markers.js93
-rw-r--r--devtools/server/tests/browser/browser_perf-recording-actor-01.js80
-rw-r--r--devtools/server/tests/browser/browser_perf-recording-actor-02.js54
-rw-r--r--devtools/server/tests/browser/browser_perf-samples-01.js63
-rw-r--r--devtools/server/tests/browser/browser_perf-samples-02.js77
-rw-r--r--devtools/server/tests/browser/browser_register_actor.js76
-rw-r--r--devtools/server/tests/browser/browser_storage_dynamic_windows.js294
-rw-r--r--devtools/server/tests/browser/browser_storage_listings.js610
-rw-r--r--devtools/server/tests/browser/browser_storage_updates.js304
-rw-r--r--devtools/server/tests/browser/browser_stylesheets_getTextEmpty.js40
-rw-r--r--devtools/server/tests/browser/browser_stylesheets_nested-iframes.js38
-rw-r--r--devtools/server/tests/browser/browser_timeline.js63
-rw-r--r--devtools/server/tests/browser/browser_timeline_actors.js69
-rw-r--r--devtools/server/tests/browser/browser_timeline_iframes.js41
-rw-r--r--devtools/server/tests/browser/director-script-target.html15
-rw-r--r--devtools/server/tests/browser/doc_allocations.html21
-rw-r--r--devtools/server/tests/browser/doc_force_cc.html29
-rw-r--r--devtools/server/tests/browser/doc_force_gc.html27
-rw-r--r--devtools/server/tests/browser/doc_innerHTML.html21
-rw-r--r--devtools/server/tests/browser/doc_perf.html25
-rw-r--r--devtools/server/tests/browser/head.js203
-rw-r--r--devtools/server/tests/browser/navigate-first.html15
-rw-r--r--devtools/server/tests/browser/navigate-second.html9
-rw-r--r--devtools/server/tests/browser/storage-dynamic-windows.html117
-rw-r--r--devtools/server/tests/browser/storage-helpers.js85
-rw-r--r--devtools/server/tests/browser/storage-listings.html123
-rw-r--r--devtools/server/tests/browser/storage-secured-iframe.html94
-rw-r--r--devtools/server/tests/browser/storage-unsecured-iframe.html26
-rw-r--r--devtools/server/tests/browser/storage-updates.html47
-rw-r--r--devtools/server/tests/browser/stylesheets-nested-iframes.html25
-rw-r--r--devtools/server/tests/browser/timeline-iframe-child.html19
-rw-r--r--devtools/server/tests/browser/timeline-iframe-parent.html11
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>