UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

858 lines (790 loc) 33 kB
/*! * 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; }));