diff --git a/plugins/angular.js b/plugins/angular.js
index f5e32f3e8..d4fb65ef4 100644
--- a/plugins/angular.js
+++ b/plugins/angular.js
@@ -37,7 +37,9 @@
(function() {
var hooked = false,
enabled = true,
- hadMissedRouteChange = false;
+ hadMissedRouteChange = false,
+ lastRouteChangeTime = 0,
+ locationChangeTrigger = false;
if (BOOMR.plugins.Angular || typeof BOOMR.plugins.SPA === "undefined") {
return;
@@ -76,6 +78,36 @@
log("Startup");
+ /**
+ * Fires the SPA route_change event.
+ *
+ * If it's been over 50ms since the last route change, fire a new
+ * route change event. Several Angular events may call this function that
+ * trigger off each other (e.g. $routeChangeStart from $locationChangeStart),
+ * so this combines them into a single route change.
+ *
+ * We also want $routeChangeStart/$stateChangeStart to trigger the route_change()
+ * if possible because those have arguments that we'll pass to the routeFilter
+ * if specified.
+ *
+ * To do this, $locationChangeStart sets a timeout before calling this
+ * that will be cleared by the $routeChangeStart/$stateChangeStart.
+ */
+ function fireRouteChange() {
+ var now = BOOMR.now();
+
+ if (now - lastRouteChangeTime > 50) {
+ BOOMR.plugins.SPA.route_change.call(null, null, arguments);
+ }
+
+ lastRouteChangeTime = now;
+
+ // clear any $locationChangeStart triggers since we've handled it
+ // either via $routeChangeStart or $stateChangeStart
+ clearTimeout(locationChangeTrigger);
+ locationChangeTrigger = false;
+ }
+
//
// Traditional Angular Router events
//
@@ -91,7 +123,7 @@
log("$routeChangeStart: " + (next ? next.templateUrl : ""));
- BOOMR.plugins.SPA.route_change(event, next, current);
+ fireRouteChange(event, next, current);
});
// Listen for $locationChangeStart to know the new URL when the route changes
@@ -104,6 +136,12 @@
BOOMR.fireEvent("spa_init", [BOOMR.plugins.SPA.current_spa_nav(), newState]);
BOOMR.plugins.SPA.last_location(newState);
+
+ // Fire a route change (on a short delay) after this callback in case
+ // $routeChangeStart never fires. We'd prefer to use $routeChangeStart's
+ // arguments (next, current) for any routeFilter, so use a setTimeout
+ // that may be cancelled by the $routeChangeStart/$stateChangeStart event.
+ locationChangeTrigger = setTimeout(fireRouteChange, 0);
});
//
@@ -117,7 +155,7 @@
log("$stateChangeStart: " + toState);
- BOOMR.plugins.SPA.route_change(event, toState, toParams, fromState, fromParams);
+ fireRouteChange(event, toState, toParams, fromState, fromParams);
});
$rootScope.$on("$stateChangeSuccess", function(event, toState, toParams, fromState, fromParams) {
diff --git a/plugins/spa.js b/plugins/spa.js
index a3a69829f..71e673ac2 100644
--- a/plugins/spa.js
+++ b/plugins/spa.js
@@ -177,12 +177,13 @@
* Called by a framework when a route change has happened
*
* @param {function} onComplete Called on completion
+ * @param {object[]} routeFilterArgs Route Filter arguments
*/
- route_change: function(onComplete) {
+ route_change: function(onComplete, routeFilterArgs) {
// if we have a routeFilter, see if they want to track this route
if (routeFilter) {
try {
- if (!routeFilter.apply(null, arguments)) {
+ if (!routeFilter.apply(null, routeFilterArgs)) {
return;
}
}
diff --git a/tests/page-templates/05-angular/108-location-change-only.html b/tests/page-templates/05-angular/108-location-change-only.html
new file mode 100644
index 000000000..378861f16
--- /dev/null
+++ b/tests/page-templates/05-angular/108-location-change-only.html
@@ -0,0 +1,34 @@
+<%= header %>
+<%= boomerangScript %>
+
+
+
+
+
+
+
+
+
+<%= footer %>
diff --git a/tests/page-templates/05-angular/108-location-change-only.js b/tests/page-templates/05-angular/108-location-change-only.js
new file mode 100644
index 000000000..62079e1a6
--- /dev/null
+++ b/tests/page-templates/05-angular/108-location-change-only.js
@@ -0,0 +1,138 @@
+/*eslint-env mocha*/
+describe("e2e/05-angular/108-location-change-only", function() {
+ var tf = BOOMR.plugins.TestFramework;
+ var t = BOOMR_test;
+
+ var pathName = window.location.pathname;
+
+ it("Should pass basic beacon validation", function(done) {
+ t.validateBeaconWasSent(done);
+ });
+
+ it("Should have sent three beacons", function() {
+ assert.equal(tf.beacons.length, 3);
+ });
+
+ it("Should have sent the first beacon as http.initiator = spa_hard", function() {
+ assert.equal(tf.beacons[0]["http.initiator"], "spa_hard");
+ });
+
+ it("Should have sent all subsequent beacons as http.initiator = spa", function() {
+ for (var i = 1; i < 2; i++) {
+ assert.equal(tf.beacons[i]["http.initiator"], "spa");
+ }
+ });
+
+ it("Should have sent all subsequent beacons have rt.nstart = navigationTiming (if NavigationTiming is supported)", function() {
+ if (typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
+ for (var i = 1; i < 2; i++) {
+ assert.equal(tf.beacons[i]["rt.nstart"], BOOMR.plugins.RT.navigationStart());
+ }
+ }
+ });
+
+ it("Should not have Boomerang timings on SPA Soft beacons", function() {
+ for (var i = 1; i < 2; i++) {
+ if (tf.beacons[i].t_other) {
+ assert.equal(tf.beacons[i].t_other.indexOf("boomr_fb"), -1, "should not have boomr_fb");
+ assert.equal(tf.beacons[i].t_other.indexOf("boomr_ld"), -1, "should not have boomr_ld");
+ assert.equal(tf.beacons[i].t_other.indexOf("boomr_lat"), -1, "should not have boomr_lat");
+ assert.equal(tf.beacons[i].t_other.indexOf("boomerang"), -1, "should not have boomerang");
+ }
+
+ // Boomerang and config timing parameters
+ assert.isUndefined(tf.beacons[i]["rt.bmr"]);
+ assert.isUndefined(tf.beacons[i]["rt.cnf"]);
+ }
+ });
+
+ //
+ // Beacon 1
+ //
+ it("Should have sent the first beacon for " + pathName, function() {
+ var b = tf.beacons[0];
+ assert.isTrue(b.u.indexOf(pathName) !== -1);
+ });
+
+ it("Should have a load time (if MutationObserver and NavigationTiming are supported)", function() {
+ if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
+ var b = tf.beacons[0];
+ assert.isDefined(b.t_done);
+ }
+ });
+
+ it("Should not have a load time (if MutationObserver is supported but NavigationTiming is not)", function() {
+ if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() === "undefined") {
+ var b = tf.beacons[0];
+ assert.equal(b.t_done, undefined);
+ }
+ });
+
+ it("Should take as long as the XHRs (if MutationObserver is not supported but NavigationTiming is)", function() {
+ if (typeof window.MutationObserver === "undefined" && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
+ t.validateBeaconWasSentAfter(0, "widgets.json", 500, 0, 30000, false);
+ }
+ });
+
+ it("Shouldn't have a load time (if MutationObserver and NavigationTiming are not supported)", function() {
+ if (typeof window.MutationObserver === "undefined" && typeof BOOMR.plugins.RT.navigationStart() === "undefined") {
+ var b = tf.beacons[0];
+ assert.equal(b.t_done, undefined);
+ assert.equal(b["rt.start"], "none");
+ }
+ });
+
+ it("Should have a t_resp of the root page (if MutationObserver and NavigationTiming are supported)", function() {
+ if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
+ var pt = window.performance.timing;
+ var b = tf.beacons[0];
+ assert.equal(b.t_resp, pt.responseStart - pt.fetchStart);
+ }
+ });
+
+ it("Should have a t_page of total - t_resp (if MutationObserver and NavigationTiming are supported)", function() {
+ if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
+ var pt = window.performance.timing;
+ var b = tf.beacons[0];
+ assert.equal(b.t_page, b.t_done - b.t_resp);
+ }
+ });
+
+ //
+ // Beacon 2
+ //
+ it("Should have sent the second beacon for /nothing", function() {
+ var b = tf.beacons[1];
+ assert.isTrue(b.u.indexOf("/nothing") !== -1);
+ });
+
+ it("Should have sent the second beacon with a timestamp of ~0 seconds (if MutationObserver is supported)", function() {
+ if (window.MutationObserver) {
+ var b = tf.beacons[1];
+ assert.closeTo(b.t_done, 0, 50);
+ }
+ });
+
+ //
+ // Beacon 3
+ //
+ it("Should have sent the third beacon for " + pathName, function() {
+ var b = tf.beacons[2];
+ assert.isTrue(b.u.indexOf(pathName) !== -1);
+ });
+
+ it("Should have sent the third with a timestamp of at around 0 seconds (if MutationObserver is supported)", function() {
+ if (window.MutationObserver) {
+ var b = tf.beacons[2];
+ assert.closeTo(b.t_done, 0, 50);
+ }
+ });
+
+ it("Should have set the same Page ID (pid) on all beacons", function() {
+ var pid = tf.beacons[0].pid;
+ for (var i = 0; i <= 2; i++) {
+ var b = tf.beacons[i];
+ assert.equal(b.pid, pid);
+ }
+ });
+});
diff --git a/tests/page-templates/05-angular/support/app.js b/tests/page-templates/05-angular/support/app.js
index a97a924ce..ae8a15019 100644
--- a/tests/page-templates/05-angular/support/app.js
+++ b/tests/page-templates/05-angular/support/app.js
@@ -1,5 +1,10 @@
/*global angular*/
-angular.module("app", ["ngResource", "ngRoute"])
+var modules = ["ngResource"];
+if (!window.angular_no_route) {
+ modules.push("ngRoute");
+}
+
+var app = angular.module("app", modules)
.factory("Widgets", ["$resource", function($resource) {
// NOTE: Using absolute urls instead of relative URLs otherwise IE11 has problems
// resolving them in html5Mode
@@ -57,12 +62,14 @@ angular.module("app", ["ngResource", "ngRoute"])
}
});
}])
-
- .config(["$routeProvider", "$locationProvider", function($routeProvider, $locationProvider) {
+ .config(["$locationProvider", function($locationProvider) {
if (typeof window.angular_html5_mode !== "undefined" && window.angular_html5_mode) {
$locationProvider.html5Mode(true);
}
+ }]);
+if (!window.angular_no_route) {
+ app.config(["$routeProvider", function($routeProvider) {
// NOTE: Using absolute urls instead of relative URLs otherwise IE11 has problems
// resolving them in html5Mode
$routeProvider.
@@ -88,63 +95,64 @@ angular.module("app", ["ngResource", "ngRoute"])
templateUrl: "/pages/05-angular/support/home.html",
controller: "mainCtrl"
});
- }])
+ }]);
+}
- .run(["$rootScope", "$location", "$timeout", function($rootScope, $location, $timeout) {
- var hadRouteChange = false;
+app.run(["$rootScope", "$location", "$timeout", function($rootScope, $location, $timeout) {
+ var hadRouteChange = false;
- $rootScope.$on("$routeChangeStart", function() {
- hadRouteChange = true;
- });
+ $rootScope.$on("$routeChangeStart", function() {
+ hadRouteChange = true;
+ });
- var hookOptions = {};
- if (window.angular_route_wait) {
- hookOptions.routeChangeWaitFilter = window.angular_route_wait;
- }
+ var hookOptions = {};
+ if (window.angular_route_wait) {
+ hookOptions.routeChangeWaitFilter = window.angular_route_wait;
+ }
- if (window.angular_route_filter) {
- hookOptions.routeFilter = window.angular_route_filter;
- }
+ if (window.angular_route_filter) {
+ hookOptions.routeFilter = window.angular_route_filter;
+ }
- function hookAngularBoomerang() {
- if (window.BOOMR && BOOMR.version) {
- if (BOOMR.plugins && BOOMR.plugins.Angular) {
- BOOMR.plugins.Angular.hook($rootScope, hadRouteChange, hookOptions);
- }
- return true;
+ function hookAngularBoomerang() {
+ if (window.BOOMR && BOOMR.version) {
+ if (BOOMR.plugins && BOOMR.plugins.Angular) {
+ BOOMR.plugins.Angular.hook($rootScope, hadRouteChange, hookOptions);
}
+ return true;
}
+ }
- if (!hookAngularBoomerang()) {
- if (document.addEventListener) {
- document.addEventListener("onBoomerangLoaded", hookAngularBoomerang);
- }
- else if (document.attachEvent) {
- document.attachEvent("onpropertychange", function(e) {
- e = e || window.event;
- if (e && e.propertyName === "onBoomerangLoaded") {
- hookAngularBoomerang();
- }
- });
- }
+ if (!hookAngularBoomerang()) {
+ if (document.addEventListener) {
+ document.addEventListener("onBoomerangLoaded", hookAngularBoomerang);
}
+ else if (document.attachEvent) {
+ document.attachEvent("onpropertychange", function(e) {
+ e = e || window.event;
+ if (e && e.propertyName === "onBoomerangLoaded") {
+ hookAngularBoomerang();
+ }
+ });
+ }
+ }
- if (typeof window.angular_nav_routes !== "undefined" &&
- Object.prototype.toString.call(window.angular_nav_routes) === "[object Array]") {
- BOOMR.subscribe("onbeacon", function(beacon) {
- // only continue for SPA beacons
- if (!BOOMR.utils.inArray(beacon["http.initiator"], BOOMR.constants.BEACON_TYPE_SPAS)) {
- return;
- }
+ if (typeof window.angular_nav_routes !== "undefined" &&
+ Object.prototype.toString.call(window.angular_nav_routes) === "[object Array]") {
+ BOOMR.subscribe("onbeacon", function(beacon) {
+ // only continue for SPA beacons
+ if (!BOOMR.utils.inArray(beacon["http.initiator"], BOOMR.constants.BEACON_TYPE_SPAS)) {
+ return;
+ }
- if (window.angular_nav_routes.length > 0) {
- var nextRoute = window.angular_nav_routes.shift();
+ if (window.angular_nav_routes.length > 0) {
+ var nextRoute = window.angular_nav_routes.shift();
- $timeout(function() {
- $location.url(nextRoute);
- }, 100);
- }
- });
- }
- }]);
+ $timeout(function() {
+ $location.url(nextRoute);
+ }, 100);
+ }
+ });
+ }
+}]);