refactor($browser): share task-tracking code between $browser and ngMock/$browser

This avoids code/logic duplication and helps the implementations stay
in-sync.
This commit is contained in:
George Kalpakas
2018-06-27 00:22:49 +03:00
parent af59a0a00b
commit 411e35472b
7 changed files with 178 additions and 181 deletions
+1
View File
@@ -41,6 +41,7 @@ var angularFiles = {
'src/ng/sanitizeUri.js',
'src/ng/sce.js',
'src/ng/sniffer.js',
'src/ng/taskTrackerFactory.js',
'src/ng/templateRequest.js',
'src/ng/testability.js',
'src/ng/timeout.js',
+2
View File
@@ -89,6 +89,7 @@
$SceProvider,
$SceDelegateProvider,
$SnifferProvider,
$$TaskTrackerFactoryProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$$TestabilityProvider,
@@ -258,6 +259,7 @@ function publishExternalAPI(angular) {
$sce: $SceProvider,
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,
$$taskTrackerFactory: $$TaskTrackerFactoryProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$$testability: $$TestabilityProvider,
+23 -96
View File
@@ -22,105 +22,27 @@
* @param {object} $log window.console or an object with the same interface.
* @param {object} $sniffer $sniffer service
*/
function Browser(window, document, $log, $sniffer) {
var ALL_TASKS_TYPE = '$$all$$',
DEFAULT_TASK_TYPE = '$$default$$';
function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
var self = this,
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
clearTimeout = window.clearTimeout,
pendingDeferIds = {},
outstandingRequestCounts = {},
outstandingRequestCallbacks = [];
taskTracker = $$taskTrackerFactory($log);
self.isMock = false;
//////////////////////////////////////////////////////////////
// Task-tracking API
//////////////////////////////////////////////////////////////
// TODO(vojta): remove this temporary api
self.$$completeOutstandingRequest = completeOutstandingRequest;
self.$$incOutstandingRequestCount = incOutstandingRequestCount;
self.$$completeOutstandingRequest = taskTracker.completeTask;
self.$$incOutstandingRequestCount = taskTracker.incTaskCount;
/**
* Executes the `fn` function and decrements the appropriate `outstandingRequestCounts` counter.
* If the counter reaches 0, all the corresponding `outstandingRequestCallbacks` are executed.
* @param {Function} fn - The function to execute.
* @param {string=} [taskType=DEFAULT_TASK_TYPE] The type of task that is being completed.
*/
function completeOutstandingRequest(fn, taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
try {
fn();
} finally {
decOutstandingRequestCount(taskType);
var countForType = outstandingRequestCounts[taskType];
var countForAll = outstandingRequestCounts[ALL_TASKS_TYPE];
// If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks.
if (!countForAll || !countForType) {
var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType;
var nextCb;
while ((nextCb = getNextCallback(taskType))) {
try {
nextCb();
} catch (e) {
$log.error(e);
}
}
}
}
}
function decOutstandingRequestCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
if (outstandingRequestCounts[taskType]) {
outstandingRequestCounts[taskType]--;
outstandingRequestCounts[ALL_TASKS_TYPE]--;
}
}
function incOutstandingRequestCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
outstandingRequestCounts[taskType] = (outstandingRequestCounts[taskType] || 0) + 1;
outstandingRequestCounts[ALL_TASKS_TYPE] = (outstandingRequestCounts[ALL_TASKS_TYPE] || 0) + 1;
}
function getLastCallback() {
var cbInfo = outstandingRequestCallbacks.pop();
return cbInfo && cbInfo.cb;
}
function getLastCallbackForType(taskType) {
for (var i = outstandingRequestCallbacks.length - 1; i >= 0; --i) {
var cbInfo = outstandingRequestCallbacks[i];
if (cbInfo.type === taskType) {
outstandingRequestCallbacks.splice(i, 1);
return cbInfo.cb;
}
}
}
function getHash(url) {
var index = url.indexOf('#');
return index === -1 ? '' : url.substr(index);
}
/**
* @private
* TODO(vojta): prefix this method with $$ ?
* @param {function()} callback Function that will be called when no outstanding request.
* @param {string=} [taskType=ALL_TASKS_TYPE] The type of tasks that will be waited for.
*/
self.notifyWhenNoOutstandingRequests = function(callback, taskType) {
taskType = taskType || ALL_TASKS_TYPE;
if (!outstandingRequestCounts[taskType]) {
callback();
} else {
outstandingRequestCallbacks.push({type: taskType, cb: callback});
}
};
// TODO(vojta): prefix this method with $$ ?
self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks;
//////////////////////////////////////////////////////////////
// URL API
@@ -140,6 +62,11 @@ function Browser(window, document, $log, $sniffer) {
cacheState();
function getHash(url) {
var index = url.indexOf('#');
return index === -1 ? '' : url.substr(index);
}
/**
* @name $browser#url
*
@@ -367,12 +294,12 @@ function Browser(window, document, $log, $sniffer) {
var timeoutId;
delay = delay || 0;
taskType = taskType || DEFAULT_TASK_TYPE;
taskType = taskType || taskTracker.DEFAULT_TASK_TYPE;
incOutstandingRequestCount(taskType);
taskTracker.incTaskCount(taskType);
timeoutId = setTimeout(function() {
delete pendingDeferIds[timeoutId];
completeOutstandingRequest(fn, taskType);
taskTracker.completeTask(fn, taskType);
}, delay);
pendingDeferIds[timeoutId] = taskType;
@@ -395,7 +322,7 @@ function Browser(window, document, $log, $sniffer) {
var taskType = pendingDeferIds[deferId];
delete pendingDeferIds[deferId];
clearTimeout(deferId);
completeOutstandingRequest(noop, taskType);
taskTracker.completeTask(noop, taskType);
return true;
}
return false;
@@ -405,8 +332,8 @@ function Browser(window, document, $log, $sniffer) {
/** @this */
function $BrowserProvider() {
this.$get = ['$window', '$log', '$sniffer', '$document',
function($window, $log, $sniffer, $document) {
return new Browser($window, $document, $log, $sniffer);
}];
this.$get = ['$window', '$log', '$sniffer', '$document', '$$taskTrackerFactory',
function($window, $log, $sniffer, $document, $$taskTrackerFactory) {
return new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory);
}];
}
+122
View File
@@ -0,0 +1,122 @@
'use strict';
/**
* ! This is a private undocumented service !
*
* @name $$taskTrackerFactory
* @description
* A function to create `TaskTracker` instances.
*
* A `TaskTracker` can keep track of pending tasks (grouped by type) and can notify interested
* parties when all pending tasks (or tasks of a specific type) have been completed.
*
* @param {$log} log - A logger instance (such as `$log`). Used to log error during callback
* execution.
*
* @this
*/
function $$TaskTrackerFactoryProvider() {
this.$get = valueFn(function(log) { return new TaskTracker(log); });
}
function TaskTracker(log) {
var self = this;
var taskCounts = {};
var taskCallbacks = [];
var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$';
var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$';
/**
* Execute the specified function and decrement the appropriate `taskCounts` counter.
* If the counter reaches 0, all corresponding `taskCallbacks` are executed.
*
* @param {Function} fn - The function to execute.
* @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed.
*/
self.completeTask = completeTask;
/**
* Increase the task count for the specified task type (or the default task type if non is
* specified).
*
* @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased.
*/
self.incTaskCount = incTaskCount;
/**
* Execute the specified callback when all pending tasks have been completed.
*
* If there are no pending tasks, the callback is executed immediately. You can optionally limit
* the tasks that will be waited for to a specific type, by passing a `taskType`.
*
* @param {function} callback - The function to call when there are no pending tasks.
* @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for.
*/
self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks;
function completeTask(fn, taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
try {
fn();
} finally {
decTaskCount(taskType);
var countForType = taskCounts[taskType];
var countForAll = taskCounts[ALL_TASKS_TYPE];
// If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks.
if (!countForAll || !countForType) {
var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType;
var nextCb;
while ((nextCb = getNextCallback(taskType))) {
try {
nextCb();
} catch (e) {
log.error(e);
}
}
}
}
}
function decTaskCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
if (taskCounts[taskType]) {
taskCounts[taskType]--;
taskCounts[ALL_TASKS_TYPE]--;
}
}
function getLastCallback() {
var cbInfo = taskCallbacks.pop();
return cbInfo && cbInfo.cb;
}
function getLastCallbackForType(taskType) {
for (var i = taskCallbacks.length - 1; i >= 0; --i) {
var cbInfo = taskCallbacks[i];
if (cbInfo.type === taskType) {
taskCallbacks.splice(i, 1);
return cbInfo.cb;
}
}
}
function incTaskCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
taskCounts[taskType] = (taskCounts[taskType] || 0) + 1;
taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1;
}
function notifyWhenNoPendingTasks(callback, taskType) {
taskType = taskType || ALL_TASKS_TYPE;
if (!taskCounts[taskType]) {
callback();
} else {
taskCallbacks.push({type: taskType, cb: callback});
}
}
}
+16 -73
View File
@@ -26,84 +26,27 @@ angular.mock = {};
* that there are several helper methods available which can be used in tests.
*/
angular.mock.$BrowserProvider = function() {
this.$get = function() {
return new angular.mock.$Browser();
};
this.$get = [
'$log', '$$taskTrackerFactory',
function($log, $$taskTrackerFactory) {
return new angular.mock.$Browser($log, $$taskTrackerFactory);
}
];
};
angular.mock.$Browser = function() {
var ALL_TASKS_TYPE = '$$all$$';
var DEFAULT_TASK_TYPE = '$$default$$';
angular.mock.$Browser = function($log, $$taskTrackerFactory) {
var self = this;
var taskTracker = $$taskTrackerFactory($log);
this.isMock = true;
self.$$url = 'http://server/';
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
// Testability API
var outstandingRequestCounts = {};
var outstandingRequestCallbacks = [];
self.$$completeOutstandingRequest = completeOutstandingRequest;
self.$$incOutstandingRequestCount = incOutstandingRequestCount;
self.notifyWhenNoOutstandingRequests = notifyWhenNoOutstandingRequests;
function decOutstandingRequestCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
if (outstandingRequestCounts[taskType]) {
outstandingRequestCounts[taskType]--;
outstandingRequestCounts[ALL_TASKS_TYPE]--;
}
}
function incOutstandingRequestCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
outstandingRequestCounts[taskType] = (outstandingRequestCounts[taskType] || 0) + 1;
outstandingRequestCounts[ALL_TASKS_TYPE] = (outstandingRequestCounts[ALL_TASKS_TYPE] || 0) + 1;
}
function completeOutstandingRequest(fn, taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
try {
fn();
} finally {
decOutstandingRequestCount(taskType);
var countForType = outstandingRequestCounts[taskType];
var countForAll = outstandingRequestCounts[ALL_TASKS_TYPE];
// If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks.
if (!countForAll || !countForType) {
var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType;
var nextCb;
while ((nextCb = getNextCallback(taskType))) {
nextCb();
}
}
}
}
function getLastCallback() {
var cbInfo = outstandingRequestCallbacks.pop();
return cbInfo && cbInfo.cb;
}
function getLastCallbackForType(taskType) {
for (var i = outstandingRequestCallbacks.length - 1; i >= 0; --i) {
var cbInfo = outstandingRequestCallbacks[i];
if (cbInfo.type === taskType) {
outstandingRequestCallbacks.splice(i, 1);
return cbInfo.cb;
}
}
}
function notifyWhenNoOutstandingRequests(callback, taskType) {
taskType = taskType || ALL_TASKS_TYPE;
if (!outstandingRequestCounts[taskType]) {
callback();
} else {
outstandingRequestCallbacks.push({type: taskType, cb: callback});
}
}
// Task-tracking API
self.$$completeOutstandingRequest = taskTracker.completeTask;
self.$$incOutstandingRequestCount = taskTracker.incTaskCount;
self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks;
// register url polling fn
@@ -131,9 +74,9 @@ angular.mock.$Browser = function() {
var timeoutId = self.deferredNextId++;
delay = delay || 0;
taskType = taskType || DEFAULT_TASK_TYPE;
taskType = taskType || taskTracker.DEFAULT_TASK_TYPE;
incOutstandingRequestCount(taskType);
taskTracker.incTaskCount(taskType);
self.deferredFns.push({
id: timeoutId,
type: taskType,
@@ -164,7 +107,7 @@ angular.mock.$Browser = function() {
if (angular.isDefined(taskIndex)) {
var task = self.deferredFns.splice(taskIndex, 1)[0];
completeOutstandingRequest(angular.noop, task.type);
taskTracker.completeTask(angular.noop, task.type);
return true;
}
@@ -198,7 +141,7 @@ angular.mock.$Browser = function() {
// Increment the time and call the next deferred function
self.defer.now = self.deferredFns[0].time;
var task = self.deferredFns.shift();
completeOutstandingRequest(task.fn, task.type);
taskTracker.completeTask(task.fn, task.type);
}
// Ensure that the current time is correct
+12 -10
View File
@@ -143,13 +143,14 @@ function MockDocument() {
}
describe('browser', function() {
/* global Browser: false */
var browser, fakeWindow, fakeDocument, fakeLog, logs;
/* global Browser: false, TaskTracker: false */
var browser, fakeWindow, fakeDocument, fakeLog, logs, taskTrackerFactory;
beforeEach(function() {
sniffer = {history: true};
fakeWindow = new MockWindow();
fakeDocument = new MockDocument();
taskTrackerFactory = function(log) { return new TaskTracker(log); };
logs = {log:[], warn:[], info:[], error:[]};
@@ -160,7 +161,8 @@ describe('browser', function() {
error: function() { logs.error.push(slice.call(arguments)); }
};
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
describe('MockBrowser', function() {
@@ -200,7 +202,7 @@ describe('browser', function() {
fakeWindow = new MockWindow({msie: msie});
fakeWindow.location.state = {prop: 'val'};
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
browser.url(fakeWindow.location.href, false, {prop: 'val'});
if (msie) {
@@ -573,7 +575,7 @@ describe('browser', function() {
// the initial URL contains a lengthy oauth token in the hash
var initialUrl = 'http://test.com/oauthcallback#state=xxx%3D&not-before-policy=0';
fakeWindow.location.href = initialUrl;
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
// somehow, $location gets a version of this url where the = is no longer escaped, and tells the browser:
var initialUrlFixedByLocation = initialUrl.replace('%3D', '=');
@@ -608,7 +610,7 @@ describe('browser', function() {
replaceState = spyOn(fakeWindow.history, 'replaceState').and.callThrough();
locationReplace = spyOn(fakeWindow.location, 'replace').and.callThrough();
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
browser.onUrlChange(function() {});
});
@@ -707,7 +709,7 @@ describe('browser', function() {
}
});
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer);
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer, taskTrackerFactory);
expect(historyStateAccessed).toBe(false);
});
@@ -720,7 +722,7 @@ describe('browser', function() {
return function() {
beforeEach(function() {
fakeWindow = new MockWindow({msie: options.msie});
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
it('should return history.state', function() {
@@ -823,7 +825,7 @@ describe('browser', function() {
return function() {
beforeEach(function() {
fakeWindow = new MockWindow({msie: options.msie});
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
it('should fire onUrlChange listeners only once if both popstate and hashchange triggered', function() {
@@ -892,7 +894,7 @@ describe('browser', function() {
function setup(options) {
fakeWindow = new MockWindow(options);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
module(function($provide, $locationProvider) {
+2 -2
View File
@@ -2873,9 +2873,9 @@ describe('$location', function() {
};
return win;
};
$browserProvider.$get = function($document, $window, $log, $sniffer) {
$browserProvider.$get = function($document, $window, $log, $sniffer, $$taskTrackerFactory) {
/* global Browser: false */
browser = new Browser($window, $document, $log, $sniffer);
browser = new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory);
browser.baseHref = function() {
return options.baseHref;
};