@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
858 lines (790 loc) • 33 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
(function(global, factory) {
"use strict";
if (typeof define === "function" && define.amd) {
// AMD
define(["URI", "sinon"], function(URI, sinon) {
return factory(URI, sinon);
});
} else {
// Global
global.RequestRecorder = factory(global.URI, global.sinon);
}
}(this, function(URI, sinon) {
"use strict";
var sModuleName = "RequestRecorder";
// resolves the given (potentially relative) URL in the same way as the browser would resolve it
function resolveURL(url) {
return new URI(url).absoluteTo(new URI(document.baseURI).search("")).toString();
}
function _privateObject() { }
_privateObject.prototype = {
//used for both modes
bIsRecording: false,
bIsPaused: false,
aEntriesUrlFilter: [],
aEntriesUrlReplace: [],
fnCustomGroupNameCallback: null,
//used for record mode only
sDefaultFilename: "Record",
aRequests: [],
mXhrNativeFunctions: {},
sFilename: "",
bIsDownloadDisabled: false,
bPromptForDownloadFilename: false,
//used in play mode only
mHarFileContent: null,
sDefaultMajorHarVersion: 1,
sDefaultCustomGroup: "defaultCustomGroup",
oSinonXhr: null,
mDelaySettings: null,
// Set default logging
oLog: {
info: function(text) {
console.info(text);
},
debug: function(text) {
console.debug(text);
},
warning: function(text) {
console.warn(text);
},
error: function(text) {
console.error(text);
}
},
/**
* The function delivers a more precise timestamp with more decimal digits.
* This timestamp is used for a better determination of the correct request/response order,
* especially if requests are asynchronous.
*
* @returns {number} Timestamp with milliseconds
*/
preciseDateNow: function() {
return window.performance.timing.navigationStart + window.performance.now();
},
/**
* Tries to load an har file from the given location URL. If no the file could not be loaded, the
* function returns null. This is used to determine if the RequestRecorder tries to record instead.
*
* If a har file is loaded, the major version is validated to match the specifications.
*
* @param {string} sLocationUrl The full URL with filename und extension.
* @returns {object|null} Har file content as JSON or null if no file is found.
*/
loadFile: function(sLocationUrl) {
// Try to request the har file from the given location url
var mHarFileContent = null;
var oRequest = new XMLHttpRequest();
oRequest.open("GET", sLocationUrl, false);
oRequest.addEventListener("load", function() {
if (this.status === 200) {
mHarFileContent = JSON.parse(this.responseText);
}
});
oRequest.send();
try {
mHarFileContent = JSON.parse(oRequest.responseText);
} catch (e) {
throw new Error("Har file could not be loaded.");
}
// Validate version of the har file
if (mHarFileContent && (!mHarFileContent.log || !mHarFileContent.log.version || parseInt(mHarFileContent.log.version, 10) != this.sDefaultMajorHarVersion)) {
this.oLog.error(sModuleName + " - Incompatible version. Please provide .har file with version " + this.sDefaultMajorHarVersion + ".x");
}
return mHarFileContent;
},
/**
* Sorts the entries of a har file by response and request order. After the entries are sorted, the function builds
* a map of the entries with the assigned custom groups and url groups.
*
* @param {object} mHarFileContent The loaded map from the har file.
* @returns {object} Prepared Har content with sorted entries and all the mappings (URL, Groups).
*/
prepareEntries: function(mHarFileContent) {
var aEntries;
if (!mHarFileContent.log.entries || !mHarFileContent.log.entries.length) {
this.oLog.info(sModuleName + " - Empty entries array or the provided har file is empty.");
aEntries = [];
} else {
// Add start and end timestamps to determine the request and response order.
aEntries = mHarFileContent.log.entries;
for (var i = 0; i < aEntries.length; i++) {
aEntries[i]._timestampStarted = new Date(aEntries[i].startedDateTime).getTime();
aEntries[i]._timestampFinished = aEntries[i]._timestampStarted + aEntries[i].time;
aEntries[i]._initialOrder = i;
}
// Sort by response first, then by request to ensure the correct order.
this.prepareEntriesOrder(aEntries, "_timestampFinished");
this.prepareEntriesOrder(aEntries, "_timestampStarted");
// Build a map with the sorted entries in the correct custom groups and by URL groups.
mHarFileContent._groupedEntries = {};
for (var j = 0; j < aEntries.length; j++) {
this.addEntryToMapping(mHarFileContent, aEntries, j);
}
}
mHarFileContent.log.entries = aEntries;
return mHarFileContent;
},
/**
* Sorts the entries by a field, if the timestamp is equal, there is a fallback to the initial order.
* This can be possible if async requests are completed at exactly the same time or if the requests are
* added manually with the same timestamps (which are created if they are not provided). If this is the case
* the initial order is important.
*
* @param {object} aEntries The Array with har file entries to sort.
* @param {string} sFieldToSort Name of the field which is sorted.
*/
prepareEntriesOrder: function(aEntries, sFieldToSort) {
aEntries.sort(function(oAEntry, oBEntry) {
var iResult = oAEntry[sFieldToSort] - oBEntry[sFieldToSort];
if (iResult === 0) {
return oAEntry._initialOrder - oBEntry._initialOrder;
} else {
return iResult;
}
});
},
/**
* Adds an entry to the mapping. The order needs to be prepared and considered when adding the entry.
* This is used to add each entry from from a provided har file or to add an entry during runtime when play is already started.
* The mapping is created at 1st by a custom group, and 2nd the method and the URL (e.g. "GET/somePath/file.js").
* The custom group is optional and determined from a provided callback (e.g. It can determine the name of a test or
* can pop from an array of names).
*
* @param {object} mHarFileContent The har file content map.
* @param {array} aEntries The entries array. Default location of har files is within mHarFileContent.log.entries.
* @param {int} iIndex The index of the entry in the entries array. This is used as the key for the mapping.
*/
addEntryToMapping: function(mHarFileContent, aEntries, iIndex) {
var sUrlGroup = this.createUrlGroup(aEntries[iIndex].request.method, aEntries[iIndex].request.url);
var customGroupName = aEntries[iIndex]._customGroupName ? aEntries[iIndex]._customGroupName : this.sDefaultCustomGroup;
if (!mHarFileContent._groupedEntries[customGroupName]) {
mHarFileContent._groupedEntries[customGroupName] = {};
}
if (!mHarFileContent._groupedEntries[customGroupName][sUrlGroup]) {
mHarFileContent._groupedEntries[customGroupName][sUrlGroup] = [];
}
mHarFileContent._groupedEntries[customGroupName][sUrlGroup].push(iIndex);
},
/**
* Creates the URL group for to map the requested XMLHttpRequests to it's response.
*
* @param {string} sMethod The http method (e.g. GET, POST...)
* @param {string} sUrl The full requested URL.
* @returns {string} The created URL group for the mapping.
*/
createUrlGroup: function(sMethod, sUrl) {
var sUrlResourcePart = new URI(sUrl).resource();
sUrlResourcePart = this.replaceEntriesUrlByRegex(sUrlResourcePart);
return sMethod + sUrlResourcePart;
},
/**
* Applies the provided Regex on the URL and replaces the needed parts.
*
* @param {string} sUrl The URL on which the replaces are applied.
* @returns {string} The new URL with the replaced parts.
*/
replaceEntriesUrlByRegex: function(sUrl) {
for (var i = 0; i < this.aEntriesUrlReplace.length; i++) {
var oEntry = this.aEntriesUrlReplace[i];
if (oEntry.regex instanceof RegExp && oEntry.value !== undefined) {
sUrl = sUrl.replace(oEntry.regex, oEntry.value);
} else {
this.oLog.warning(sModuleName + " - Invalid regular expression for url replace.");
}
}
return sUrl;
},
/**
* Prepares the data from an XMLHttpRequest for saving into an har file. All needed data to replay the request
* is collected (e.g. time, headers, response, status).
*
* @param {Object} oXhr The finished XMLHttpRequest, from which the data for the har file is extracted.
* @param {number} fStartTimestamp The request start timestamp.
* @returns {Object} The prepared entry for the har file.
*/
prepareRequestForHar: function(oXhr, fStartTimestamp) {
var oEntry = {
startedDateTime: new Date(fStartTimestamp).toISOString(),
time: this.preciseDateNow() - fStartTimestamp,
request: {
headers: oXhr._requestParams.headers,
url: resolveURL(oXhr._requestParams.url),
method: oXhr._requestParams.method
},
response: {
status: oXhr.status,
content: {
text: oXhr.responseText
}
}
};
if (oXhr._requestParams.customGroupName) {
oEntry._customGroupName = oXhr._requestParams.customGroupName;
}
oEntry.response.headers = this.transformHeadersFromArrayToObject(oXhr);
return oEntry;
},
/**
* Transforms the headers from an XMLHttpRequest to the expected har file format.
* The origin format is a string which will be transformed to an object with name and value properties.
* E.g. { name: "headername", value: "headervalue" }
*
* @param {Object} oXhr The XMLHttpRequest with response headers (needs to be sent).
* @returns {Array} The result array with the headers as objects.
*/
transformHeadersFromArrayToObject: function(oXhr) {
var aTemp = oXhr.getAllResponseHeaders().split("\r\n");
var aHeadersObjects = [];
for (var i = 0; i < aTemp.length; i++) {
if (aTemp[i]) {
var aHeaderValues = aTemp[i].split(":");
aHeadersObjects.push({
name: aHeaderValues[0].trim(),
value: aHeaderValues[1].trim()
});
}
}
return aHeadersObjects;
},
/**
* Delete the current recordings
*/
deleteRecordedEntries: function() {
this.aRequests = [];
},
/**
* Transforms and delivers the recorded data for the har file.
* If the downloading is not disabled the file will be downloaded automatically or with an optional prompt for a filename.
*
* @param {boolean} bDeleteRecordings True if the existing entries should be deleted.
* @returns {Object} The recorded har file content
*/
getHarContent: function(bDeleteRecordings) {
// Check for the filename or ask for it (if configured)
var sFilename = (this.sFilename || this.sDefaultFilename);
if (this.bPromptForDownloadFilename) {
sFilename = window.prompt("Enter file name", sFilename + ".har");
} else {
sFilename = sFilename + ".har";
}
// The content skeleton
var mHarContent = {
log: {
version: "1.2",
creator: {
name: "RequestRecorder",
version: "1.0"
},
entries: this.aRequests
}
};
// Check if recorded entries should be cleared.
if (bDeleteRecordings) {
this.deleteRecordedEntries();
}
// Inject the data into the dom and download it (if configured).
if (!this.bIsDownloadDisabled) {
var sString = JSON.stringify(mHarContent, null, 4);
var a = document.createElement("a");
document.body.appendChild(a);
var oBlob = new window.Blob([sString], { type: "octet/stream" });
var sUrl = window.URL.createObjectURL(oBlob);
a.href = sUrl;
a.download = sFilename;
a.click();
window.URL.revokeObjectURL(sUrl);
}
return mHarContent;
},
/**
* Calculates the delay on base of the provided settings.
* It is possible to configure an offset, a minimum and a maximum delay.
*
* @param {object} mDelaySettings The settings map.
* @param {int} iTime The curreent duration of the request as milliseconds.
* @returns {int} The new duration as milliseconds.
*/
calculateDelay: function(mDelaySettings, iTime) {
if (mDelaySettings) {
if (mDelaySettings.factor !== undefined && typeof mDelaySettings.factor === 'number') {
iTime *= mDelaySettings.factor;
}
if (mDelaySettings.offset !== undefined && typeof mDelaySettings.offset === 'number') {
iTime += mDelaySettings.offset;
}
if (mDelaySettings.max !== undefined && typeof mDelaySettings.max === 'number') {
iTime = Math.min(mDelaySettings.max, iTime);
}
if (mDelaySettings.min !== undefined && typeof mDelaySettings.min === 'number') {
iTime = Math.max(mDelaySettings.min, iTime);
}
}
return iTime;
},
/**
* Responds to a given FakeXMLHttpRequest object with an entry from a har file.
* If a delay is provided, the time is calculated and the response of async requests will be delayed.
* Sync requests can not be deleayed.
*
* @param {Object} oXhr FakeXMLHttpRequest to respond.
* @param {Object} oEntry Entry from the har file.
*/
respond: function(oXhr, oEntry) {
var fnRespond = function() {
if (oXhr.readyState !== 0) {
var sResponseText = oEntry.response.content.text;
// Transform headers to the required format for XMLHttpRequests.
var oHeaders = {};
oEntry.response.headers.forEach(function(mHeader) {
oHeaders[mHeader.name] = mHeader.value;
});
// Support for injected callbacks
if (typeof sResponseText === "function") {
sResponseText = sResponseText();
}
oXhr.respond(
oEntry.response.status,
oHeaders,
sResponseText
);
}
};
// If the request is async, a possible delay will be applied.
if (oXhr.async) {
// Create new browser task with the setTimeout function to make sure that responses of async requests are not delievered too fast.
setTimeout(function() {
fnRespond();
}, this.calculateDelay(this.mDelaySettings, oEntry.time));
} else {
fnRespond();
}
},
/**
* Checks a URL against an array of regex if its filtered.
* If the RequestRecorder is paused, the URL is filtered, too.
*
* @param {string} sUrl URL to check.
* @param {RegExp[]} aEntriesUrlFilter Array of regex filters.
* @returns {boolean} If the URL is filterd true is returned.
*/
isUrlFiltered: function(sUrl, aEntriesUrlFilter) {
if (this.bIsPaused) {
return true;
}
var that = this;
return aEntriesUrlFilter.every(function(regex) {
if (regex instanceof RegExp) {
return !regex.test(sUrl);
} else {
that.oLog.error(sModuleName + " - Invalid regular expression for filter.");
return true;
}
});
},
/**
* Initilizes the RequestRecorder with the provided options, otherwise all default options will be set.
* This method is used to init and also to RESET the needed paramters before replay and recording.
*
* It restores sinon and the native XHR functions which are overwritten during the recording.
*
* @param {object} mOptions The options parameter from the public API (start, play, record).
*/
init: function(mOptions) {
mOptions = mOptions || {};
if (typeof mOptions !== "object") {
throw new Error("Parameter object isn't a valid object");
}
// Reset all parameters to default
this.mHarFileContent = null;
this.aRequests = [];
this.sFilename = "";
this.bIsRecording = false;
this.bIsPaused = false;
this.bIsDownloadDisabled = false;
if (this.oSinonXhr) {
this.oSinonXhr.filters = this.aSinonFilters;
this.aSinonFilters = [];
this.oSinonXhr.restore();
this.oSinonXhr = null;
}
// Restore native XHR functions if they were overwritten.
for (var sFunctionName in this.mXhrNativeFunctions) {
if (this.mXhrNativeFunctions.hasOwnProperty(sFunctionName)) {
window.XMLHttpRequest.prototype[sFunctionName] = this.mXhrNativeFunctions[sFunctionName];
}
}
this.mXhrNativeFunctions = {};
// Set options to provided values or to default
this.bIsDownloadDisabled = mOptions.disableDownload === true;
this.bPromptForDownloadFilename = mOptions.promptForDownloadFilename === true;
if (mOptions.delay) {
if (mOptions.delay === true) {
this.mDelaySettings = {}; // Use delay of recording
} else {
this.mDelaySettings = mOptions.delay;
}
} else {
this.mDelaySettings = { max: 0 }; // default: no delay
}
if (mOptions.entriesUrlFilter) {
if (Array.isArray(mOptions.entriesUrlFilter)) {
this.aEntriesUrlFilter = mOptions.entriesUrlFilter;
} else {
this.aEntriesUrlFilter = [mOptions.entriesUrlFilter];
}
} else {
this.aEntriesUrlFilter = [new RegExp(".*")]; // default: no filtering
}
if (mOptions.entriesUrlReplace) {
if (Array.isArray(mOptions.entriesUrlReplace)) {
this.aEntriesUrlReplace = mOptions.entriesUrlReplace;
} else {
this.aEntriesUrlReplace = [mOptions.entriesUrlReplace];
}
} else {
this.aEntriesUrlReplace = [];
}
if (mOptions.customGroupNameCallback && typeof mOptions.customGroupNameCallback === "function") {
this.fnCustomGroupNameCallback = mOptions.customGroupNameCallback;
} else {
this.fnCustomGroupNameCallback = function() { return false; }; // default: Empty Callback function used
}
},
/**
* Checks if the player is started.
* If FakeXMLHttprequest from sinon is enabled from the RequestRecorder, it should be in play mode.
*
* @returns {boolean} True if the replay is started.
*/
isPlayStarted: function() {
return !!this.oSinonXhr;
},
/**
* Checks if the recorder is started.
*
* @returns {boolean} True if recording is started.
*/
isRecordStarted: function() {
return this.bIsRecording;
}
};
/**
* Instance of the RequestRecorder's private part.
*
* @type {object} The private functions.
* @private
*/
var _private = new _privateObject();
/**
* Instance of the RequestRecorder's public part (API).
*
* @type {object}
*/
var RequestRecorder = {
/**
* Start with existing locationUrl or inline entries results in a playback. If the file does not exist, XMLHttpRequests are not faked and the recording starts.
*
* @param {string|Array} locationUrl Specifies from which location the file is loaded. If it is not found, the recording is started. The provided filename is the name of the output har file.
* This parameter can be the entries array for overloading the function.
* @param {object} [options] Contains optional parameters to config the RequestRecorder:
* {boolean|object} [options.delay] If a the parameter is equals true, the recorded delay timings are used, instead of the default delay equals zero. If a map as parameter is used, the delay is calculated with the delaysettings in the object. Possible settings are max, min, offset, factor.
* {function} [options.customGroupNameCallback] A callback is used to determine the custom groupname of the current XMLHttpRequest. If the callback returns a falsy value, the default groupname is used.
* {boolean} [options.disableDownload] Set this flag to true if you don´t want to download the recording after the recording is finished. This parameter is only used for testing purposes.
* {boolean} [options.promptForDownloadFilename] Activates a prompt popup after stop is called to enter a desired filename.
* {array|RegExp} [options.entriesUrlFilter] A list of regular expressions, if it matches the URL the request-entry is filtered.
* array|object} [options.entriesUrlReplace] A list of objects with regex and value to replace. E.g.: "{ regex: new RegExp("RegexToSearchForInUrl"), "value": "newValueString" }"
*/
start: function(locationUrl, options) {
try {
// Try to start play-mode
this.play(locationUrl, options);
} catch (e) {
// If play-mode could not be started, try to record instead.
var oUri = new URI(locationUrl);
var sExtension = oUri.suffix();
// Check if the provided URL is a har file, maybe the wrong url is provided
if (sExtension != "har") {
_private.oLog.warning(sModuleName + " - Invalid file extension: " + sExtension + ", please use '.har' files.");
}
this.record(oUri.filename().replace("." + sExtension, ""), options);
}
},
/**
* Start recording with a desired filename and the required options.
*
* @param {string|object} filename The name of the har file to be recorded.
* @param {object} [options] Contains optional parameters to config the RequestRecorder:
* {function} [options.customGroupNameCallback] A callback is used to determine the custom groupname of the current XMLHttpRequest. If the callback returns a falsy value, the default groupname is used.
* boolean} [options.disableDownload] Set this flag to true if you don´t want to download the recording after the recording is finished. This parameter is only used for testing purposes.
* {boolean} [options.promptForDownloadFilename] Activates a prompt popup after stop is called to enter a desired filename.
* {array|RegExp} [options.entriesUrlFilter] A list of regular expressions, if it matches the URL the request-entry is filtered.
*/
record: function(filename, options) {
_private.oLog.info(sModuleName + " - Record");
if (window.XMLHttpRequest.name === 'FakeXMLHttpRequest') {
_private.oLog.warning(sModuleName + " - Sinon FakeXMLHttpRequest is enabled by another application, recording could be defective");
}
if (_private.isRecordStarted()) {
_private.oLog.error(sModuleName + " - RequestRecorder is already recording, please stop first...");
return;
}
_private.init(options);
_private.sFilename = filename;
_private.bIsRecording = true;
// Overwrite the open method to get the required request parameters (method, URL, headers) and assign
// a group name if provided.
_private.mXhrNativeFunctions.open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function() {
this._requestParams = this._requestParams || {};
this._requestParams.method = arguments[0];
this._requestParams.url = arguments[1];
this._requestParams.customGroupName = _private.fnCustomGroupNameCallback();
this._requestParams.headers = this._requestParams.headers || [];
_private.mXhrNativeFunctions.open.apply(this, arguments);
};
// Overwrite the setRequestHeader method to record the request headers.
_private.mXhrNativeFunctions.setRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function(sHeaderName, sHeaderValue) {
this._requestParams = this._requestParams || { headers: [] };
this._requestParams.headers.push({
name: sHeaderName,
value: sHeaderValue
});
_private.mXhrNativeFunctions.setRequestHeader.apply(this, arguments);
};
// Overwrite the send method to get the response and the collected data from the XMLHttpRequest
_private.mXhrNativeFunctions.send = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.send = function() {
if (!_private.isUrlFiltered(this._requestParams.url, _private.aEntriesUrlFilter)) {
var fTimestamp = _private.preciseDateNow();
// If the onreadystatechange is already specified by another application, it is called, too.
var fnOldStateChanged = this.onreadystatechange;
this.onreadystatechange = function() {
if (this.readyState === 4) {
_private.aRequests.push(_private.prepareRequestForHar(this, fTimestamp));
_private.oLog.info(
sModuleName + " - Record XMLHttpRequest. Method: " + this._requestParams.method + ", URL: " + this._requestParams.url
);
}
if (fnOldStateChanged) {
fnOldStateChanged.apply(this, arguments);
}
};
}
_private.mXhrNativeFunctions.send.apply(this, arguments);
};
},
/**
* Start replay with a complete URL to the har file or with an entries array and the required options.
*
* @param {string|Array} locationUrlOrEntriesArray Specifies from which location the file is loaded. This parameter is overloaded and can also be an entries array.
* @param {object} [options] Contains optional parameters to config the RequestRecorder:
* {boolean|object} [options.delay] If a the parameter is equals true, the recorded delay timings are used, instead of the default delay equals zero. If a map as parameter is used, the delay is calculated with the delaysettings in the object. Possible settings are max, min, offset, factor.
* {function} [options.customGroupNameCallback] A callback is used to determine the custom group name of the current XMLHttpRequest. If the callback returns a falsy value, the default group name is used.
* {array|RegExp} [options.entriesUrlFilter] A list of regular expressions, if it matches the URL the request-entry is filtered.
* {array|object} [options.entriesUrlReplace] A list of objects with regex and value to replace. E.g.: "{ regex: new RegExp("RegexToSearchForInUrl"), "value": "newValueString" }"
*/
play: function(locationUrlOrEntriesArray, options) {
_private.oLog.info(sModuleName + " - Play");
if (_private.isPlayStarted()) {
_private.oLog.error(sModuleName + " - RequestRecorder is already playing, please stop first...");
return;
}
_private.init(options);
var sLocationUrl;
// Check if locationUrl parameter is entries array
// Decide if entries are provided or if a file needs to be loaded.
if (locationUrlOrEntriesArray && Array.isArray(locationUrlOrEntriesArray)) {
_private.mHarFileContent = {};
_private.mHarFileContent.log = { "entries": locationUrlOrEntriesArray.slice(0) };
sLocationUrl = "";
} else {
sLocationUrl = locationUrlOrEntriesArray;
_private.mHarFileContent = _private.loadFile(sLocationUrl);
}
// Provided entries or har file entries must be prepared for usage with the RequestRecorder.
if (_private.mHarFileContent) {
_private.mHarFileContent = _private.prepareEntries(_private.mHarFileContent);
_private.oLog.info(sModuleName + " - Har file found, replay started (" + sLocationUrl + ")");
// If entries are found, start the player
_private.oSinonXhr = sinon.useFakeXMLHttpRequest();
_private.oSinonXhr.useFilters = true;
// Wrapping of Sinon filters, because also sap.ui.core.util.MockServer also use the same sinon instance
_private.aSinonFilters = _private.oSinonXhr.filters;
_private.oSinonXhr.filters = [];
_private.oSinonXhr.addFilter(function(sMethod, sUrl, bAsync, sUsername, sPassword) {
if (!_private.isUrlFiltered(sUrl, _private.aEntriesUrlFilter)) {
return false;
}
for (var i = 0; i < _private.aSinonFilters.length; i++) {
if (_private.aSinonFilters[i](sMethod, sUrl, bAsync, sUsername, sPassword) === false) {
_private.oLog.debug(sModuleName + " - Foreign URL filter from sinon filters are applied.");
return false;
}
}
return true;
});
var fnOnCreate = _private.oSinonXhr.onCreate;
_private.oSinonXhr.onCreate = function(oXhr) {
var fnXhrSend = oXhr.send;
oXhr.send = function() {
if (!_private.isUrlFiltered(oXhr.url, _private.aEntriesUrlFilter)) {
var oEntry;
var mCustomGroup;
// Get next entry
var sUrl = resolveURL(oXhr.url);
sUrl = new URI(sUrl).resource();
sUrl = _private.replaceEntriesUrlByRegex(sUrl);
var sUrlGroup = oXhr.method + sUrl;
var customGroupName = _private.fnCustomGroupNameCallback();
if (!customGroupName) {
customGroupName = _private.sDefaultCustomGroup;
}
if (!_private.mHarFileContent._groupedEntries[customGroupName]) {
throw new Error("Custom group name does not exist: " + customGroupName);
}
mCustomGroup = _private.mHarFileContent._groupedEntries[customGroupName];
if (!mCustomGroup[sUrlGroup]) {
throw new Error("URL does not exist: " + sUrlGroup);
}
if (!mCustomGroup[sUrlGroup].length) {
throw new Error("No more entries left for: " + sUrlGroup);
}
oEntry = _private.mHarFileContent.log.entries[mCustomGroup[sUrlGroup].shift()];
_private.oLog.info(sModuleName + " - Respond XMLHttpRequest. Method: " + oXhr.method + ", URL: " + sUrl);
_private.respond(oXhr, oEntry);
} else {
fnXhrSend.apply(this, arguments);
}
};
// sinon onCreate call. MockServer use the onCreate hook to the onSend to the xhr.
if (fnOnCreate) {
fnOnCreate.apply(this, arguments);
}
};
}
},
/**
* Stops the recording or the player.
* If downloading is not disabled, the har file is downloaded automatically.
*
* @returns {Object|null} In record mode the har file is returned as json, otherwise null is returned.
*/
stop: function() {
_private.oLog.info(sModuleName + " - Stop");
var mHarContent = null;
if (_private.isRecordStarted()) {
mHarContent = _private.getHarContent(true);
}
// do this for a full cleanup
_private.init();
return mHarContent;
},
/**
* Pause the replay or recording.
* Can be used to exclude XMLHttpRequests.
*/
pause: function() {
_private.oLog.info(sModuleName + " - Pause");
_private.bIsPaused = true;
},
/**
* Continues the replay or recording.
*/
resume: function() {
_private.oLog.info(sModuleName + " - Resume");
_private.bIsPaused = false;
},
/**
* Delivers the current recordings in HAR format during record mode and the recording is not aborted.
* Requests which are not completed with readyState 4 are not included.
*
* @param {boolean} [deleteRecordings] True if the recordings should be deleted.
* @returns {Object} The har file as json.
*/
getRecordings: function(deleteRecordings) {
var bDeleteRecordings = deleteRecordings || false;
_private.oLog.info(sModuleName + " - Get Recordings");
return _private.getHarContent(bDeleteRecordings);
},
/**
* Adds a JSON encoded response for a request with the provided URL.
*
* @param {string} url The requested URL.
* @param {string|function} response The returned response as string or callback.
* @param {string} [method] The HTTP method (e.g. GET, POST), default is GET.
* @param {int} [status] The desired status, default is 200.
* @param {array} [headers] The response Headers, the Content-Type for JSON is already set for this method.
*/
addResponseJson: function(url, response, method, status, headers) {
var aHeaders = headers || [];
aHeaders.push({
"name": "Content-Type",
"value": "application/json;charset=utf-8"
});
this.addResponse(url, response, method, status, aHeaders);
},
/**
* Adds a response for a request with the provided URL.
*
* @param {string} url The requested URL.
* @param {string|function} response The returned response as string or callback.
* @param {string} [method] The HTTP method (e.g. GET, POST), default is GET.
* @param {int} [status] The desired status, default is 200.
* @param {array} [headers] The response Headers, default is text/plain.
*/
addResponse: function(url, response, method, status, headers) {
if (!_private.isPlayStarted()) {
throw new Error("Start the player first before you add a response.");
}
var sMethod = method || "GET";
var aHeaders = headers || [{
"name": "Content-Type",
"value": "text/plain;charset=utf-8"
}];
var iStatus = status || 200;
var oEntry = {
"startedDateTime": new Date().toISOString(),
"time": 0,
"request": {
"headers": [],
"url": url,
"method": sMethod
},
"response": {
"status": iStatus,
"content": {
"text": response
},
"headers": aHeaders
}
};
var iIndex = _private.mHarFileContent.log.entries.push(oEntry) - 1;
_private.addEntryToMapping(_private.mHarFileContent, _private.mHarFileContent.log.entries, iIndex);
},
/**
* Sets a custom logger for the log messages of the RequestRecorder.
* The logger objects needs to implement the following functions: info, debug, warning, error
*
* @param {object} logger The log object with the required functions.
*/
setLogger: function(logger) {
if (typeof logger != "object"
|| typeof logger.info != "function"
|| typeof logger.debug != "function"
|| typeof logger.warning != "function"
|| typeof logger.error != "function"
) {
throw new Error("Logger is not valid. It should implement at least the functions: info, debug, warning, error.");
}
_private.oLog = logger;
}
};
return RequestRecorder;
}));