/*!
 * ${copyright}
 */
sap.ui.define([
	"sap/base/Log",
	"sap/base/util/uid",
	"sap/ui/Device",
	"sap/ui/base/ManagedObjectObserver",
	"sap/ui/base/SyncPromise",
	"sap/ui/core/library",
	"sap/ui/core/message/Message",
	"sap/ui/core/mvc/Controller",
	"sap/ui/core/mvc/View",
	"sap/ui/model/BindingMode",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/ui/model/Sorter",
	"sap/ui/model/json/JSONModel",
	"sap/ui/model/odata/CountMode",
	"sap/ui/model/odata/MessageScope",
	"sap/ui/model/odata/v2/ODataModel",
	"sap/ui/test/TestUtils",
	"sap/ui/thirdparty/datajs",
	"sap/ui/util/XMLHelper"
	// load Table resources upfront to avoid loading times > 1 second for the first test using Table
	// "sap/ui/table/Table"
], function (Log, uid, Device, ManagedObjectObserver, SyncPromise, coreLibrary, Message, Controller,
		View, BindingMode, Filter, FilterOperator, Sorter, JSONModel, CountMode, MessageScope,
		ODataModel, TestUtils, datajs, XMLHelper) {
	/*global QUnit, sinon*/
	/*eslint max-nested-callbacks: 0, no-warning-comments: 0, quote-props: 0*/
	"use strict";

	var sDefaultLanguage = sap.ui.getCore().getConfiguration().getLanguage(),
		MessageType = coreLibrary.MessageType, // shortcut for sap.ui.core.MessageType
		NO_CONTENT = {/*204 no content*/},
		sODataModelClassName = "sap.ui.model.odata.v2.ODataModel",
		// determine the row in which the entity is expected from the context path
		rRowIndex = /~(\d+)~/,
		/**
		 * Maps back-end response severity values to the values defined in the enumeration
		 * <code>sap.ui.core.MessageType</code>.
		 */
		mSeverityMap = {
			error : MessageType.Error,
			warning : MessageType.Warning,
			success : MessageType.Success,
			info : MessageType.Information
		},
		rTemporaryKey = /id(?:-[0-9]+){2}/;

	/**
	 * Clones the given OData message object and replaces the target property of the clone by
	 * the given target path.
	 *
	 * @param {object} oODataMessage
	 *   An OData message object as returned by <code>createResponseMessage</code>
	 * @param {string} sTarget
	 *   The new target
	 * @param {string[]} aAdditionalTargets
	 *   The new additional targets in case of a multi-target message
	 * @returns {object}
	 *   The cloned OData message object with the replaced target
	 */
	function cloneODataMessage(oODataMessage, sTarget, aAdditionalTargets) {
		return Object.assign({}, oODataMessage,
			{target : sTarget, additionalTargets : aAdditionalTargets});
	}

	/**
	 * Creates an error response object for a technical error (http status code = 4xx/5xx).
	 *
	 * @param {object} [oErrorResponseInfo]
	 *   The object describing the error response
	 * @param {boolean} [oErrorResponseInfo.crashBatch]
	 *   Whether the complete batch request shall fail
	 * @param {string} [oErrorResponseInfo.message="Internal Server Error"]
	 *   The message text
	 * @param {string} [oErrorResponseInfo.messageCode="UF0"]
	 *   The message code
	 * @param {int} [oErrorResponseInfo.statusCode=500]
	 *   The HTTP status code
	 * @returns {object}
	 *   The error response
	 */
	function createErrorResponse(oErrorResponseInfo) {
		oErrorResponseInfo = oErrorResponseInfo || {};

		return {
			body : JSON.stringify({
				error : {
					code : oErrorResponseInfo.messageCode || "UF0",
					message : {
						value : oErrorResponseInfo.message || "Internal Server Error"
					}
				}
			}),
			crashBatch : oErrorResponseInfo.crashBatch,
			headers : {"Content-Type" : "application/json;charset=utf-8"},
			statusCode : oErrorResponseInfo.statusCode || 500,
			statusText : "FAILED"
		};
	}

	/**
	 * Creates a V2 OData model.
	 *
	 * @param {string} sServiceUrl
	 *   The service URL
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction
	 *   {@link sap.ui.model.odata.v2.ODataModel#constructor}. The standard ODataModel behavior will
	 *   be overwritten by the following default test parameters:
	 * @param {sap.ui.model.odata.CountMode} [mModelParameters.defaultCountMode=None]
	 *   Sets the default count mode for the model
	 * @param {boolean} [mModelParameters.useBatch=false]
	 *   Whether all requests should be sent in batch requests
	 * @returns {sap.ui.model.odata.v2.ODataModel} The model
	 */
	function createModel(sServiceUrl, mModelParameters) {
		var mDefaultParameters = {
				defaultCountMode : CountMode.None,
				serviceUrl : sServiceUrl,
				useBatch : false
			};

		return new ODataModel(Object.assign(mDefaultParameters, mModelParameters));
	}

	/**
	 * Creates a V2 OData model for the Workcenter Groups service.
	 *
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction. The default parameters are set in the createModel
	 *   function.
	 * @returns {sap.ui.model.odata.v2.ODataModel}
	 *   The model
	 */
	function createPPWorkcenterGroupModel(mModelParameters) {
		return createModel("/sap/opu/odata/sap/PP_WORKCENTER_GROUP_SRV", mModelParameters);
	}

	/**
	 * Creates a V2 OData model for the RMT sample flight service.
	 *
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction. The default parameters are set in the createModel
	 *   function.
	 * @returns {sap.ui.model.odata.v2.ODataModel}
	 *   The model
	 */
	function createRMTSampleFlightModel(mModelParameters) {
		return createModel("/sap/opu/odata/IWBEP/RMTSAMPLEFLIGHT", mModelParameters);
	}

	/**
	 * Creates a V2 OData model for <code>GWSAMPLE_BASIC</code>.
	 *
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction. The default parameters are set in the createModel
	 *   function.
	 * @returns {sap.ui.model.odata.v2.ODataModel}
	 *   The model
	 */
	function createSalesOrdersModel(mModelParameters) {
		return createModel("/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/", mModelParameters);
	}

	/**
	 * Creates a V2 OData model for <code>GWSAMPLE_BASIC</code> supporting message scope, which
	 * means it has <code>sap:message-scope-supported="true"</code> at the
	 * <code>EntityContainer</code> in its <code>$metadata</code>.
	 *
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction. The default parameters are set in the createModel
	 *   function.
	 * @returns {sap.ui.model.odata.v2.ODataModel}
	 *   The model
	 */
	function createSalesOrdersModelMessageScope(mModelParameters) {
		return createModel("/SalesOrderSrv/", mModelParameters);
	}

	/**
	 * Creates a V2 OData model for <code>UI_C_DFS_ALLWNCREQ</code> service to test hierarchies with
	 * <code>ODataTreeBinding</code>.
	 *
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction. The default parameters are set in the createModel
	 *   function.
	 * @returns {sap.ui.model.odata.v2.ODataModel}
	 *   The model
	 */
	function createAllowanceModel(mModelParameters) {
		return createModel("/sap/opu/odata/sap/UI_C_DFS_ALLWNCREQ/", mModelParameters);
	}

	/**
	 * Creates a V2 OData model for special cases.
	 *
	 * @param {object} [mModelParameters]
	 *   Map of parameters for model construction. The default parameters are set in the createModel
	 *   function.
	 * @returns {sap.ui.model.odata.v2.ODataModel}
	 *   The model
	 */
	function createSpecialCasesModel(mModelParameters) {
		return createModel("/special/cases/", mModelParameters);
	}

	/**
	 * Gets a string representation of the given messages to be used in "sap-message" response
	 * header. In case of multiple messages, the first message is the outer message and the other
	 * messages are stored as inner messages in the "details" property.
	 *
	 * @param {object|object[]} vMessage
	 *   A message object or an array of message objects as returned by an OData V2 service.
	 * @returns {string}
	 *   A stringified representation of the given messages
	 */
	function getMessageHeader(vMessage) {
		var bIsArray = Array.isArray(vMessage),
			oMessage = bIsArray ? vMessage[0] : vMessage;

		return JSON.stringify(Object.assign(
			oMessage, {details : bIsArray ? vMessage.slice(1) : []}
		));
	}

	/**
	 * Delays the execution of a given callback function and returns its result within a Promise.
	 *
	 * @param {function} [fnCallback]
	 *   A callback function
	 * @param {number} [iDelay=5]
	 *   A delay in milliseconds
	 * @returns {Promise}
	 *   A promise which resolves with the result of the given callback or undefined after the given
	 *   delay
	 */
	function resolveLater(fnCallback, iDelay) {
		return new Promise(function (resolve) {
			setTimeout(function () {
				resolve(fnCallback && fnCallback());
			}, iDelay || 5);
		});
	}

	/**
	 * Wraps the given XML string into a <View> and creates an XML document for it. Verifies that
	 * the sap.m.Table does not use <items>, because this is the default aggregation and may be
	 * omitted. (This ensures that <ColumnListItem> is a direct child.)
	 *
	 * If the binding uses <ColumnListItem>, <columns> is not allowed. The columns are automatically
	 * determined from the number of the elements in <ColumnListItem>.
	 *
	 * @param {string} sViewXML The view content as XML string
	 * @returns {Document} The view as XML document
	 */
	function xml(sViewXML) {
		var oDocument;

		oDocument = XMLHelper.parse(
			'<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" xmlns:t="sap.ui.table">'
			+ sViewXML
			+ '</mvc:View>',
			"application/xml"
		);
		xmlConvertMTables(oDocument);
		xmlConvertGridTables(oDocument);

		return oDocument;
	}

	/**
	 * Converts the sap.ui.table.(Table|TreeTable) controls within the document. Embeds all inner
	 * controls into a <t:Column> with <t:template> each. <t:Column> may still be used however.
	 * Do not use <rows>, it breaks this automatic conversion (and is unnecessary anyway).
	 *
	 * @param {Document} oDocument The view as XML document
	 */
	function xmlConvertGridTables(oDocument) {
		function convertElements(aElements) {
			var oChildNode, aChildNodes, oColumn, oElement, i, j, oTemplate;

			for (i = aElements.length - 1; i >= 0; i -= 1) {
				oElement = aElements[i];

				aChildNodes = oElement.childNodes;
				for (j = aChildNodes.length - 1; j >= 0; j -= 1) {
					oChildNode = aChildNodes[j];
					if (oChildNode.nodeType === Node.ELEMENT_NODE
							&& oChildNode.localName !== "Column") {
						oColumn = document.createElementNS("sap.ui.table", "Column");
						oElement.insertBefore(oColumn, oChildNode);
						oElement.removeChild(oChildNode);
						oTemplate = document.createElementNS("sap.ui.table", "template");
						oColumn.appendChild(oTemplate);
						oTemplate.appendChild(oChildNode);
					}
				}
			}
		}

		convertElements(oDocument.getElementsByTagNameNS("sap.ui.table", "Table"));
		convertElements(oDocument.getElementsByTagNameNS("sap.ui.table", "TreeTable"));
	}

	/**
	 * Converts the sap.m.Table controls within the document. Embeds all inner controls into a
	 * <ColumnListItem>. <ColumnListItem> may still be used however. Do not use <items>, it breaks
	 * this automatic conversion (and is unnecessary anyway). Do not use <columns>, they are added
	 * automatically.
	 *
	 * @param {Document} oDocument The view as XML document
	 */
	function xmlConvertMTables(oDocument) {
		var aControls, oChildNode, aChildNodes, iColumnCount, aColumnNodes, oColumnsElement,
			oElement, bHasColumns, bHasListItem, i, j, k, aTableElements;

		aTableElements = oDocument.getElementsByTagNameNS("sap.m", "Table");
		iColumnCount = 0;
		for (i = aTableElements.length - 1; i >= 0; i -= 1) {
			oElement = aTableElements[i];
			aControls = [];

			aChildNodes = oElement.childNodes;
			for (j = aChildNodes.length - 1; j >= 0; j -= 1) {
				oChildNode = aChildNodes[j];
				switch (oChildNode.nodeName) {
					case "columns":
						bHasColumns = true;
						break;
					case "items":
						throw new Error("Do not use <items> in sap.m.Table");
					case "ColumnListItem":
						aColumnNodes = oChildNode.childNodes;

						bHasListItem = true;
						for (k = aColumnNodes.length - 1; k >= 0; k -= 1) {
							if (aColumnNodes[k].nodeType === Node.ELEMENT_NODE) {
								iColumnCount += 1;
							}
						}
						break;
					default:
						if (oChildNode.nodeType === Node.ELEMENT_NODE) {
							oElement.removeChild(oChildNode);
							aControls.unshift(oChildNode);
							iColumnCount += 1;
						}
				}
			}
			if (iColumnCount) {
				if (bHasColumns) {
					throw new Error("Do not use <columns> in sap.m.Table");
				}
				if (aControls.length) {
					if (bHasListItem) {
						throw new Error("Do not use controls w/ and w/o <ColumnListItem>"
							+ " in sap.m.Table");
					}
					oColumnsElement = document.createElementNS("sap.m", "ColumnListItem");
					for (j = 0; j < aControls.length; j += 1) {
						oColumnsElement.appendChild(aControls[j]);
					}
					oElement.appendChild(oColumnsElement);
				}
				oColumnsElement = oDocument.createElementNS("sap.m", "columns");
				while (iColumnCount > 0) {
					oColumnsElement.appendChild(oDocument.createElementNS("sap.m", "Column"));
					iColumnCount -= 1;
				}
				oElement.appendChild(oColumnsElement);
			}
		}
	}

	//*********************************************************************************************
	QUnit.module("sap.ui.model.odata.v2.ODataModel.integration", {
		beforeEach : function () {
			// We use a formatter to check for property changes. However before the formatter is
			// called, the value is passed through the type's formatValue
			// (see PropertyBinding#_toExternalValue). Ensure that this result is predictable.
			sap.ui.getCore().getConfiguration().setLanguage("en-US");

			// These metadata files are _always_ faked, the query option "realOData" is ignored
			TestUtils.useFakeServer(this._oSandbox, "sap/ui/core", {
				"/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/$metadata"
					: {source : "qunit/model/GWSAMPLE_BASIC.metadata.xml"},
				"/sap/opu/odata/sap/ZUI5_GWSAMPLE_BASIC/$metadata?foo=bar&customMeta=custom%2Fmeta"
					: {source : "internal/samples/odata/v2/Products/data/metadata.xml"},
				"/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/annotations.xml"
					: {source : "qunit/model/GWSAMPLE_BASIC.annotations.xml"},
				// GWSAMPLE_BASIC service with sap:message-scope-supported="true"
				"/SalesOrderSrv/$metadata"
					: {source : "qunit/testdata/SalesOrder/metadata.xml"},
				"/sap/opu/odata/sap/PP_WORKCENTER_GROUP_SRV/$metadata"
					: {source : "qunit/model/PP_WORKCENTER_GROUP_SRV.metadata.xml"},
				"/sap/opu/odata/IWBEP/RMTSAMPLEFLIGHT/$metadata"
					: {source : "qunit/model/RMTSAMPLEFLIGHT.withMessageScope.metadata.xml"},
				"/sap/opu/odata/sap/UI_C_DFS_ALLWNCREQ/$metadata"
					: {source : "qunit/odata/v2/data/UI_C_DFS_ALLWNCREQ.metadata.xml"},
				"/special/cases/$metadata"
					: {source : "qunit/odata/v2/data/metadata_special_cases.xml"}
			});
			this.oLogMock = this.mock(Log);
			this.oLogMock.expects("warning")
				.withExactArgs(sinon.match.string, "LegacyParametersGet", "sap.ui.support",
					sinon.match.func)
				.atLeast(0);
			this.oLogMock.expects("error").never();
			this.oLogMock.expects("fatal").never();

			// Counter for batch requests
			this.iBatchNo = 0;
			// {map<string, string[]>}
			// this.mChanges["id"] is a list of expected changes for the property "text" of the
			// control with ID "id"
			this.mChanges = {};
			// counter for OData messages created during a test
			this.iODataMessageCount = 0;
			// {map<string, true>}
			// If an ID is in this.mIgnoredChanges, change events with null are ignored
			this.mIgnoredChanges = {};
			// {map<string, string[][]>}
			// this.mListChanges["id"][i] is a list of expected changes for the property "text" of
			// the control with ID "id" in row i
			this.mListChanges = {};
			// A list of expected messages
			this.aMessages = [];
			// The number of pending responses checkFinish has to wait for
			this.iPendingResponses = 0;
			// A list of expected requests with the properties method, url, headers, response
			this.aRequests = [];
			// The temporary key for a created entity; can be referenced in deep paths by ~key~
			this.sTemporaryKey = undefined;

			// If the "VisibleRowCountMode" of the sap.ui.table.* is "Auto", the table uses the
			// screen height (Device.resize.height) to compute the amount of contexts it requests
			// initially. Make sure that this is stable across devices.
			this._oSandbox.stub(Device.resize, "height").value(1000);
		},

		afterEach : function (assert) {
			if (this.oView) {
				// avoid calls to formatters by UI5 localization changes in later tests
				this.oView.destroy();
			}
			if (this.oModel) {
				this.oModel.destroy();
			}
			// reset the language
			sap.ui.getCore().getConfiguration().setLanguage(sDefaultLanguage);
		},

		/**
		 * Checks the messages and finishes the test if no pending changes are left, all
		 * expected requests have been received and the expected number of messages have been
		 * reported.
		 *
		 * @param {object} assert The QUnit assert object
		 */
		checkFinish : function (assert) {
			var sControlId, aExpectedValuesPerRow, i;

			if (this.aRequests.length || this.iPendingResponses) {
				return;
			}
			for (sControlId in this.mChanges) {
				if (!this.hasOnlyOptionalChanges(sControlId)) {
					if (this.mChanges[sControlId].length) {
						return;
					}
					delete this.mChanges[sControlId];
				}
			}
			for (sControlId in this.mListChanges) {
				// Note: This may be a sparse array
				aExpectedValuesPerRow = this.mListChanges[sControlId];
				for (i in aExpectedValuesPerRow) {
					if (aExpectedValuesPerRow[i].length) {
						return;
					}
					delete aExpectedValuesPerRow[i];
				}
				delete this.mListChanges[sControlId];
			}
			if (sap.ui.getCore().getUIDirty()
					|| sap.ui.getCore().getMessageManager().getMessageModel().getObject("/").length
						< this.aMessages.length) {
				setTimeout(this.checkFinish.bind(this, assert), 10);

				return;
			}
			if (this.resolve) {
				this.resolve();
				this.resolve = null;
			}
		},

		/**
		 * Checks that exactly the expected messages have been reported, the order doesn't matter.
		 *
		 * @param {object} assert The QUnit assert object
		 */
		checkMessages : function (assert) {
			var aCurrentMessages = sap.ui.getCore().getMessageManager().getMessageModel()
					.getObject("/").sort(compareMessages),
				aExpectedMessages = this.aMessages.slice().sort(compareMessages);

			function compareMessages(oMessage1, oMessage2) {
				return oMessage1.message.localeCompare(oMessage2.message);
			}

			/*
			 * Maps the given message object to an object containing only the properties relevant
			 * for comparing expected and actual message leaving out properties like "id".
			 */
			function mapMessage(oMessage) {
				return {
					code : oMessage.code,
					description : oMessage.description,
					descriptionUrl : oMessage.descriptionUrl,
					aFullTargets : oMessage.aFullTargets.map(function (sFullTarget) {
						return sFullTarget.replace(rTemporaryKey, "~key~");
					}),
					message : oMessage.message,
					persistent : oMessage.persistent,
					aTargets : oMessage.aTargets.map(function (sTarget) {
						return sTarget.replace(rTemporaryKey, "~key~");
					}),
					technical : oMessage.technical,
					type : oMessage.type
				};
			}

			// check only a subset of properties
			aCurrentMessages = aCurrentMessages.map(mapMessage);
			aExpectedMessages = aExpectedMessages.map(mapMessage);

			assert.deepEqual(aCurrentMessages, aExpectedMessages,
				this.aMessages.length + " expected messages in message manager");
		},

		/**
		 * Checks that the given value is the expected one for the control.
		 *
		 * @param {object} assert The QUnit assert object
		 * @param {string} sValue The value
		 * @param {string} sControlId The control ID
		 * @param {number|string} [vRow] The row index in case the control's binding is below a
		 *   list binding or the path of the row's context (for example in the tests of the
		 *   ODataMetaModel), otherwise <code>undefined</code>.
		 */
		checkValue : function (assert, sValue, sControlId, vRow) {
			var sExpectedValue,
				iRow = (typeof vRow === "string")
					? Number(rRowIndex.exec(vRow)[1])
					: vRow,
				aExpectedValues = (iRow === undefined)
					? this.mChanges[sControlId]
					: this.mListChanges[sControlId] && this.mListChanges[sControlId][iRow],
				sVisibleId = iRow === undefined ? sControlId : sControlId + "[" + iRow + "]";

			if (!aExpectedValues || !aExpectedValues.length) {
				if (!(sControlId in this.mIgnoredChanges && sValue === null)) {
					assert.ok(false, sVisibleId + ": " + JSON.stringify(sValue) + " (unexpected)");
				}
			} else {
				sExpectedValue = aExpectedValues.shift();
				// Note: avoid bad performance of assert.strictEqual(), e.g. DOM manipulation
				if (sValue !== sExpectedValue || iRow === undefined || typeof iRow !== "number"
						|| iRow < 10) {
					assert.strictEqual(sValue, sExpectedValue,
						sVisibleId + ": " + JSON.stringify(sValue));
				}
			}
			this.checkFinish(assert);
		},

		/**
		 * Checks the control's value state after waiting some time for the control to set it.
		 *
		 * @param {object} assert The QUnit assert object
		 * @param {string|sap.m.InputBase} vControl The control ID or an instance of InputBase
		 * @param {sap.ui.core.ValueState} sState The expected value state
		 * @param {string} sText The expected text
		 *
		 * @returns {Promise} A promise resolving when the check is done
		 */
		checkValueState : function (assert, vControl, sState, sText) {
			var oControl = typeof vControl === "string" ? this.oView.byId(vControl) : vControl;

			return resolveLater(function () {
				assert.strictEqual(oControl.getValueState(), sState,
					oControl.getId() + ": value state: " + oControl.getValueState());
				assert.strictEqual(oControl.getValueStateText(), sText,
					oControl.getId() + ": value state text: " + oControl.getValueStateText());
			});
		},

		/**
		 * Searches the incoming request in the list of expected requests by comparing the URL.
		 * Removes the found request from the list.
		 *
		 * @param {object} oActualRequest The actual request
		 * @returns {object} The matching expected request or undefined if none was found
		 */
		consumeExpectedRequest : function (oActualRequest) {
			var oExpectedRequest, i;

			if (this.aRequests.length === 1) {
				return this.aRequests.shift(); // consume the one and only candidate to get a diff
			}
			for (i = 0; i < this.aRequests.length; i += 1) {
				oExpectedRequest = this.aRequests[i];
				if (oExpectedRequest.requestUri === oActualRequest.requestUri) {
					this.aRequests.splice(i, 1);
					return oExpectedRequest;
				}
			}
		},

		/**
		 * Creates an OData message object that can be passed as input parameter to
		 * <code>getMessageHeader</code>.
		 *
		 * @param {string|string[]} [vTarget]
		 *   The target or an array of targets in case of a multi-target message
		 * @param {string} [sMessage="message-~i~"]
		 *   The message text; if not given, "message-~i~" is used, where ~i~ is a generated number
		 * @param {string} [sSeverity="error"]
		 *   The message severity; either "error", "warning", "success" or "info"
		 * @param {boolean} [bTransition]
		 *   Whether the message is a transition message
		 * @returns {object}
		 *   An OData message object with following properties: <code>code</code> with the value
		 *   "code-~i~" (where ~i~ is a generated number), <code>message</code>,
		 *   <code>severity</code>, <code>target</code> and <code>transition</code>
		 */
		createResponseMessage : function (vTarget, sMessage, sSeverity, bTransition) {
			var i = this.iODataMessageCount,
				oResponseMessage;

			this.iODataMessageCount += 1;

			oResponseMessage = {
				code : "code-" + i,
				message : sMessage || "message-" + i,
				severity : sSeverity || "error",
				transition : bTransition
			};
			if (vTarget !== undefined) {
				if (Array.isArray(vTarget)) {
					if (vTarget.length > 1) {
						oResponseMessage.additionalTargets = vTarget.slice(1);
					}
					vTarget = vTarget[0];
				}
				oResponseMessage.target = vTarget;
			}

			return oResponseMessage;
		},

		/**
		 * Creates the view and attaches it to the model. Checks that the expected requests (see
		 * {@link #expectRequest} are fired and the controls got the expected changes (see
		 * {@link #expectChange}).
		 *
		 * @param {object} assert The QUnit assert object
		 * @param {string} sViewXML The view content as XML
		 * @param {sap.ui.model.odata.v2.ODataModel|Object<string,object>} [vModel] The model resp.
		 *   a map of named models (default model is undefined); the models are attached to the view
		 *   and to the test instance.
		 *   If no model is given, <code>createSalesOrdersModel</code> is used.
		 * @param {object} [oController]
		 *   An object defining the methods and properties of the controller
		 * @returns {Promise} A promise that is resolved when the view is created and all expected
		 *   values for controls have been set
		 */
		createView : function (assert, sViewXML, vModel, oController) {
			var mNamedModels,
				that = this;

			/**
			 * Stub function for datajs#request. Decides if a request should be executed as single
			 * request or batch request.
			 *
			 * @param {object} oRequest The request object
			 * @param {function} fnSuccess Success callback function
			 * @param {function} fnError Error callback function
			 * @param {object} oHandler The request handler object
			 * @param {object} oHttpClient The HttpClient object
			 * @param {object} oMetadata The metadata object
			 */
			function checkRequest(oRequest, fnSuccess, fnError, oHandler, oHttpClient, oMetadata) {
				if (oRequest.requestUri.includes("$batch")) {
					checkBatchRequest(oRequest, fnSuccess, fnError);
				} else {
					checkSingleRequest(oRequest, fnSuccess, fnError);
				}
			}

			/**
			 * Checks that all requests in a batch are as expected and handles its response.
			 *
			 * @param {object} oRequest The request object
			 * @param {function} fnSuccess Success callback function
			 * @param {function} fnError Error callback function
			 */
			function checkBatchRequest(oRequest, fnSuccess, fnError) {
				var oCrashedResponse;

				/**
				 * Processes a request within a batch
				 * @param {object} oRequest The request
				 * @returns {object} The processed response object of datajs#request
				 */
				function processRequest(oRequest) {
					if (oRequest.__changeRequests) {
						return Promise.all(
							oRequest.__changeRequests.map(processRequest)
						).then(function (aResponses) {
							var oErrorResponse = aResponses.reduce(function (oReduced, oCurrent) {
									return oReduced || oCurrent.message && oCurrent;
								}, undefined);

							if (oErrorResponse) {
								return oErrorResponse;
							}
							return {__changeResponses : aResponses};
						});
					}
					return checkSingleRequest(oRequest, function /*fnSuccess*/(oData, oResponse) {
							return oResponse;
						},
						function /*fnError*/(oError) {
							return {message : "HTTP request failed", response : oError.response};
						},
						that.iBatchNo
					).then(function (oResponse) {
						if (oResponse.response && oResponse.response.crashBatch) {
							delete oResponse.response.crashBatch;
							oCrashedResponse = oResponse;
						}
						return oResponse;
					});
				}

				/**
				 * @param {object} oRequest A batch request object that contains an array of
				 *   requests
				 */
				function processRequests(oRequest) {
					var aRequests = oRequest.data.__batchRequests;

					Promise.all(
						aRequests.map(processRequest)
					).then(function (aResponses) {
						var oBatchResponse;

						if (oCrashedResponse) {
							fnError(oCrashedResponse);
						} else {
							oBatchResponse = {
								data : {
									__batchResponses : aResponses
								}
							};

							fnSuccess(oBatchResponse.data, oBatchResponse);
						}
					});
				}

				that.iBatchNo += 1;

				processRequests(oRequest);
			}

			/**
			 * Checks that the expected request arrived and handles its response. If the status of
			 * the expected request is less than 300 the given success handler is called, otherwise
			 * the given error handler is called. This function can also be used to check requests
			 * within a $batch request. In this case the resulting promise is resolved with the
			 * return value of the given success or error handler.
			 *
			 * @param {object} oActualRequest
			 *   The request object
			 * @param {function} fnSuccess
			 *   Success callback function
			 * @param {function} fnError
			 *   Error callback function
			 * @param {number} [iBatchNo]
			 *   The number of the batch to which the request belongs to
			 * @returns {Promise}
			 *   Returns a Promise resolving with the result of the given success or error callback
			 */
			function checkSingleRequest(oActualRequest, fnSuccess, fnError, iBatchNo) {
				var sContentID,
					oExpectedRequest,
					oExpectedResponse,
					mHeaders,
					sMethod = oActualRequest.method,
					oResponse,
					mResponseHeaders,
					sUrl = oActualRequest.requestUri,
					bWaitForResponse = true;

				function checkFinish() {
					if (!that.aRequests.length && !that.iPendingResponses) {
						// give some time to process the response
						setTimeout(that.checkFinish.bind(that, assert), 0);
					}
				}

				function _getResponseMetadata(sRequestUri, iIndex) {
					sRequestUri = sRequestUri.split("?")[0];

					return {
						uri : (iIndex === undefined)
							? sRequestUri
							: sRequestUri + "('~" + iIndex + "~')"
					};
				}

				oActualRequest = Object.assign({}, oActualRequest);
				oActualRequest.headers = Object.assign({}, oActualRequest.headers);
				sContentID = oActualRequest.contentID;

				if (sUrl.startsWith(that.oModel.sServiceUrl)) {
					oActualRequest.requestUri = sUrl.slice(that.oModel.sServiceUrl.length + 1);
				}
				oExpectedRequest = that.consumeExpectedRequest(oActualRequest);

				mHeaders = oActualRequest.headers;
				delete mHeaders["Accept"];
				delete mHeaders["Accept-Language"];
				delete mHeaders["Content-Type"];
				delete mHeaders["DataServiceVersion"];
				delete mHeaders["MaxDataServiceVersion"];
				delete mHeaders["sap-cancel-on-close"];
				delete mHeaders["sap-contextid-accept"];
				delete oActualRequest["_handle"];
				delete oActualRequest["adjustDeepPath"];
				delete oActualRequest["async"];
				delete oActualRequest["deferred"];
				delete oActualRequest["eventInfo"];
				delete oActualRequest["expandRequest"];
				delete oActualRequest["functionMetadata"];
				delete oActualRequest["functionTarget"];
				delete oActualRequest["password"];
				delete oActualRequest["requestID"];
				delete oActualRequest["updateAggregatedMessages"];
				delete oActualRequest["user"];
				delete oActualRequest["contentID"];
				if (oExpectedRequest) {
					oExpectedResponse = oExpectedRequest.response;

					if (oExpectedResponse === NO_CONTENT) {
						oResponse = {
							statusCode : 204
						};
					} else if (oExpectedResponse
							&& (oExpectedResponse.statusCode < 200
								|| oExpectedResponse.statusCode >= 300)) {
						oResponse = {
							response : oExpectedResponse
						};
					} else if (oExpectedResponse && typeof oExpectedResponse.then === "function") {
						oResponse = oExpectedResponse;
					} else {
						oResponse = oExpectedResponse && oExpectedResponse.data
							? oExpectedResponse
							: {data : oExpectedResponse, statusCode : 200};

						// oResponse needs __metadata for ODataModel.prototype._getKey
						if (oResponse.data && Array.isArray(oResponse.data.results)) {
							oResponse.data.results.forEach(function (oResponseItem, i) {
								oResponseItem.__metadata = oResponseItem.__metadata
									|| _getResponseMetadata(oExpectedRequest.requestUri, i);
							});
						} else if (oExpectedRequest.method !== "HEAD") {
							oResponse.data.__metadata = oResponse.data.__metadata
								|| _getResponseMetadata(oExpectedRequest.requestUri);
						}
					}

					bWaitForResponse = !(oResponse && typeof oResponse.then === "function");
					delete oExpectedRequest.response;
					mResponseHeaders = oExpectedRequest.responseHeaders;
					delete oExpectedRequest.responseHeaders;

					if (oActualRequest.key && sMethod !== "MERGE"
							&& oActualRequest.headers["x-http-method"] !== "MERGE") {
						that.sTemporaryKey = sContentID
							|| oActualRequest.key.match(rTemporaryKey)[0];

						oExpectedRequest.deepPath = oExpectedRequest.deepPath.replace("~key~",
							that.sTemporaryKey);
						delete oActualRequest["key"];

						if (oExpectedRequest.headers && oExpectedRequest.headers["Content-ID"]) {
							oExpectedRequest.headers["Content-ID"] =
								oExpectedRequest.headers["Content-ID"]
									.replace("~key~", that.sTemporaryKey);
						}
					}
					if (oActualRequest.requestUri.startsWith("$") && sMethod === "GET") {
						oExpectedRequest.requestUri = oExpectedRequest.requestUri.replace("~key~",
							that.sTemporaryKey);
						oExpectedRequest.deepPath = oExpectedRequest.deepPath.replace("~key~",
							that.sTemporaryKey);
					}
					if ("batchNo" in oExpectedRequest) {
						oActualRequest.batchNo = iBatchNo;
					}
					assert.deepEqual(oActualRequest, oExpectedRequest, sMethod + " " + sUrl);
					oResponse.headers = mResponseHeaders || {};
					if (oExpectedRequest.headers["Content-ID"]) {
						oResponse.headers["Content-ID"] = oExpectedRequest.headers["Content-ID"];
					}
				} else {
					assert.ok(false, sMethod + " " + sUrl + " (unexpected)");
					oResponse = {value : []}; // dummy response to avoid further errors
				}

				if (bWaitForResponse) {
					that.iPendingResponses += 1;
				} else {
					checkFinish();
				}

				return Promise.resolve(oResponse).then(function (oResponseBody) {
					if (oResponseBody.statusCode >= 200 && oResponseBody.statusCode < 300) {
						return fnSuccess({}, oResponseBody);
					} else {
						return fnError(oResponseBody);
					}
				}).finally(function () {
					if (bWaitForResponse) {
						that.iPendingResponses -= 1;
					}
					// Waiting may be over after the promise has been handled
					checkFinish();
				});
			}

			mNamedModels = vModel && !(vModel instanceof sap.ui.model.Model)
				? vModel
				: {undefined : vModel || createSalesOrdersModel()};
			this.oModel = mNamedModels.undefined;
			this.mock(datajs).expects("request").atLeast(0).callsFake(checkRequest);
			//assert.ok(true, sViewXML); // uncomment to see XML in output, in case of parse issues

			return View.create({
				type : "XML",
				controller : oController && new (Controller.extend(uid(), oController))(),
				definition : xml(sViewXML)
			}).then(function (oView) {
				var sModelName;

				Object.keys(that.mChanges).forEach(function (sControlId) {
					var oControl = oView.byId(sControlId);

					if (oControl) {
						that.observe(assert, oControl, sControlId);
					}
				});
				Object.keys(that.mListChanges).forEach(function (sControlId) {
					var oControl = oView.byId(sControlId);

					if (oControl) {
						that.observe(assert, oControl, sControlId, true);
					}
				});

				for (sModelName in mNamedModels) {
					sModelName = sModelName === "undefined" ? undefined : sModelName;
					oView.setModel(mNamedModels[sModelName], sModelName);
				}
				// enable parse error messages in the message manager
				sap.ui.getCore().getMessageManager().registerObject(oView, true);
				// Place the view in the page so that it is actually rendered. In some situations,
				// esp. for the table.Table this is essential.
				oView.placeAt("qunit-fixture");
				that.oView = oView;

				return that.waitForChanges(assert);
			});
		},

		/**
		 * The following code (either {@link #createView} or anything before
		 * {@link #waitForChanges}) is expected to set a value (or multiple values) at the property
		 * "text" of the control with the given ID. <code>vValue</code> must be a list with expected
		 * values for each row if the control is created via a template in a list.
		 *
		 * You must call the function before {@link #createView}, even if you do not expect a change
		 * to the control's value initially. This is necessary because createView must attach a
		 * formatter function to the binding info before the bindings are created in order to see
		 * the change. If you expect a value at a later time but not initially, set the vValue
		 * parameter to <code>null</code>.
		 *
		 * Examples:
		 * this.expectChange("foo", "bar"); // expect value "bar" for the control with ID "foo"
		 * this.expectChange("foo", false); // listen to changes for the control with ID "foo", but
		 *                                  // do not expect a change (in createView). To be used if
		 *                                  // the control is a template within a table.
		 * this.expectChange("foo", ["a", "b"]); // expect values for two rows of the control with
		 *                                       // ID "foo"; may be combined with an offset vRow
		 * this.expectChange("foo", ["a",,"b"]); // expect values for the rows 0 and 2 of the
		 *                                       // control with the ID "foo", because this is a
		 *                                       // sparse array in which index 1 is unset
		 * this.expectChange("foo", "c", 2); // expect value "c" for control with ID "foo" in row 2
		 * this.expectChange("foo", "d", "/MyEntitySet/ID");
		 *                                 // expect value "d" for control with ID "foo" in a
		 *                                 // metamodel table on "/MyEntitySet/ID"
		 * this.expectChange("foo", "bar").expectChange("foo", "baz"); // expect 2 changes for "foo"
		 * this.expectChange("foo", null, null); // table.Table sets the binding context on an
		 *                                       // existing row to null when scrolling
		 * this.expectChange("foo", null); // row is deleted in table.Table so that its context is
		 *                                 // destroyed; this can also be used to listen to control
		 *                                 // changes that are not expected; a controls with a
		 *                                 // property binding (e.g. Text/Input) that expects
		 *                                 // changes must be initialized with <code>null</code>
		 *
		 * @param {string} sControlId The control ID
		 * @param {string|string[]|boolean|null} [vValue] The expected value, a list of expected
		 *   values, <code>false</code> to enforce listening to a template control or
		 *   <code>null</code> to initialize a control for a later change.
		 * @param {number|string} [vRow] The row index (for the model) or the path of its parent
		 *   context (for the metamodel), in case that a change is expected for a single row of a
		 *   list (in this case <code>vValue</code> must be a string).
		 * @returns {object} The test instance for chaining
		 * @throws {Error} If {@link #expectValue} is used in the same test
		 */
		expectChange : function (sControlId, vValue, vRow) {
			if (this.bCheckValue === true) {
				throw Error("Must not call expectChange after using expectValue in a test");
			}

			this.bCheckValue = false;
			return this.expectChangeInternal.apply(this, arguments);
		},

		/**
		 * Expects the code following a call to this method to set a value on the given control just
		 * like {@link #expectChange} with the difference that values given in the
		 * <code>vValue</code> parameter must be provided in <em>external</em> format as the
		 * corresponding change is checked by attaching a {@link sap.ui.base.ManagedObjectObserver}
		 * to the control's text or value property.
		 *
		 * @param {string} sControlId The control ID
		 * @param {string|string[]|boolean|null} [vValue] The expected value, a list of expected
		 *   values, <code>false</code> to enforce listening to a template control or
		 *   <code>null</code> to initialize a control for a later change.
		 * @param {number|string} [vRow] The row index (for the model) or the path of its parent
		 *   context (for the metamodel), in case that a change is expected for a single row of a
		 *   list (in this case <code>vValue</code> must be a string).
		 * @returns {object} The test instance for chaining
		 * @throws {Error} If {@link #expectChange} is used in the same test
		 */
		expectValue : function (sControlId, vValue, vRow) {
			if (this.bCheckValue === false) {
				throw Error("Must not call expectValue after using expectChange in a test");
			}

			this.bCheckValue = true;
			return this.expectChangeInternal.apply(this, arguments);
		},

		/**
		 * Implementation of methods {@link #expectChange} and {@link #expectValue}; see
		 * documentation of these for a description.
		 */
		expectChangeInternal : function (sControlId, vValue, vRow) {
			var aExpectations, i;

			// Ensures that oObject[vProperty] is an array and returns it
			function array(oObject, vProperty) {
				oObject[vProperty] = oObject[vProperty] || [];

				return oObject[vProperty];
			}

			if (arguments.length === 3) {
				aExpectations = array(this.mListChanges, sControlId);
				if (Array.isArray(vValue)) {
					for (i = 0; i < vValue.length; i += 1) {
						if (i in vValue) {
							// This may create a sparse array this.mListChanges[sControlId]
							array(aExpectations, vRow + i).push(vValue[i]);
						}
					}
				} else {
					// This may create a sparse array this.mListChanges[sControlId]
					array(aExpectations, vRow).push(vValue);
				}
			} else if (Array.isArray(vValue)) {
				aExpectations = array(this.mListChanges, sControlId);
				for (i = 0; i < vValue.length; i += 1) {
					if (i in vValue) {
						array(aExpectations, i).push(vValue[i]);
					}
				}
			} else if (vValue === false) {
				array(this.mListChanges, sControlId);
			} else {
				aExpectations = array(this.mChanges, sControlId);
				if (arguments.length > 1) {
					aExpectations.push(vValue);
				}
			}

			return this;
		},

		/**
		 * The following code (either {@link #createView} or anything before
		 * {@link #waitForChanges}) is expected to perform a <code>HEAD<code> request.
		 *
		 * @param {object} [mAdditionalHeaders]
		 *   Request headers additional to the "x-csrf-token" header
		 * @returns {object}
		 *   The test instance for chaining
		 */
		expectHeadRequest : function (mAdditionalHeaders) {
			this.aRequests.push({
				deepPath : "",
				headers : Object.assign({"x-csrf-token" : "Fetch"}, mAdditionalHeaders),
				method : "HEAD",
				requestUri : ""
			});

			return this;
		},

		/**
		 * Adds a message to the array of expected messages for this test based on the given OData
		 * message object, the target prefix and the full target prefix. If the given oODataMessage
		 * is <code>null</code>, no message is added. That allows using the "?" operator in
		 * <code>expectMessage</code> calls for messages that depend on the test fixture. No extra
		 * <code>if</code> statement is needed.
		 *
		 * @param {object} oODataMessage
		 *   An OData message object as returned by <code>createResponseMessage</code> or
		 *   <code>null</code>
		 * @param {string|string[]|object} vTargetPrefix
		 *   The prefix for the target; if vTargetPrefix is not of type string or an array of
		 *   strings the given object may have following properties: <code>path</code> and
		 *   <code>isComplete</code>;  note that a given array is modified
		 * @param {boolean} vTargetPrefix.isComplete
		 *   Whether <code>vTargetPrefix.path</code> is the complete message target or
		 *   <code>vTargetPrefix.path</code> is a prefix for the <code>oODataMessage.target</code>
		 * @param {string} vTargetPrefix.path
		 *   A path or a path prefix for the target
		 * @param {string|string[]} [vFullTargetPrefix=vTargetPrefix]
		 *   The prefix for the full target; if not given <code>vTargetPrefix</code> is also used as
		 *   prefix for the <code>fullTarget</code>; if vTargetPrefix is an array of strings the
		 *   vFullTargetPrefix must be given as an array with the equivalent number of strings; note
		 *   that a given array is modified
		 * @param {boolean} [bResetMessages]
		 *   Whether existing expected messages are reset before the new message is added
		 * @returns {object}
		 *   The test instance for chaining
		 */
		expectMessage : function (oODataMessage, vTargetPrefix, vFullTargetPrefix, bResetMessages) {
			var aAdditionalTargets,
				aFullTargets,
				sTargetPrefix,
				aTargets;

			function computeFullTarget(sODataMessageTarget) {
				return Array.isArray(vFullTargetPrefix)
					? vFullTargetPrefix.shift() + sODataMessageTarget
					: (vFullTargetPrefix || sTargetPrefix) + sODataMessageTarget;
			}

			function computeTarget(sODataMessageTarget) {
				if (Array.isArray(vTargetPrefix)) {
					sTargetPrefix = vTargetPrefix.shift();
				}
				return vTargetPrefix.isComplete
					? vTargetPrefix.path
					: sTargetPrefix + sODataMessageTarget;
			}

			if (bResetMessages) {
				this.aMessages = [];
			}

			if (oODataMessage !== null) {
				sTargetPrefix = vTargetPrefix.isComplete ? "" : vTargetPrefix.path || vTargetPrefix;
				aAdditionalTargets = oODataMessage.additionalTargets || [];
				aTargets = [computeTarget(oODataMessage.target)]
					.concat(aAdditionalTargets.map(computeTarget));
				aFullTargets = [computeFullTarget(oODataMessage.target)]
					.concat(aAdditionalTargets.map(computeFullTarget));

				this.aMessages.push(new Message({
					code : oODataMessage.code,
					description : oODataMessage.description,
					descriptionUrl : "",
					fullTarget : aFullTargets,
					message : oODataMessage.message,
					persistent : false,
					target : aTargets,
					technical : false,
					type : mSeverityMap[oODataMessage.severity]
				}));
			}

			return this;
		},

		/**
		 * The following code (either {@link #createView} or anything before
		 * {@link #waitForChanges}) is expected to report exactly the given messages. All expected
		 * messages should have a different message text.
		 *
		 * @param {object|object[]} vExpectedMessages
		 *   The expected message or an array of expected messages (with properties code, message,
		 *   target, persistent, technical and type corresponding the getters of
		 *   sap.ui.core.message.Message)
		 * @returns {object}
		 *   The test instance for chaining
		 */
		expectMessages : function (vExpectedMessages) {
			//TODO expectMessages takes sap.ui.core.message.Message like objects while
			// expectMessage takes an ODataMessage like object => make uniform, separate change
			// this.aMessages = [];
			//
			// if (!Array.isArray(vExpectedMessages)) {
			// 	vExpectedMessages = [vExpectedMessages];
			// }
			// vExpectedMessages.forEach(this.expectMessage.bind(this));
			if (!Array.isArray(vExpectedMessages)) {
				vExpectedMessages = [vExpectedMessages];
			}
			this.aMessages = vExpectedMessages.map(function (oMessage) {
				oMessage.description = oMessage.hasOwnProperty("description")
					?  oMessage.description
					: undefined;
				oMessage.descriptionUrl = oMessage.hasOwnProperty("descriptionUrl")
					?  oMessage.descriptionUrl
					: "";
				oMessage.technical = oMessage.technical || false;
				return new Message(oMessage);
			});

			return this;
		},

		/**
		 * The following code (either {@link #createView} or anything before
		 * {@link #waitForChanges}) is expected to perform the given request. <code>oResponse</code>
		 * describes how to react on the request. Usually you simply give the JSON for the response
		 * and the request will be responded in the next microtask.
		 *
		 * <code>oResponse</code> may also be a promise resolving with the response or the error. In
		 * this case you can control the response time (typically to control the order of the
		 * responses).
		 *
		 * @param {string|object} vRequest
		 *   The request with the mandatory properties "deepPath" and "requestUri".
		 *   Optional properties are:
		 *   <ul>
		 *     <li>"batchNo": The batch number in which the request is contained</li>
		 *     <li>"encodeRequestUri": Whether the query string of the requestUri has to be encoded;
		 *       <code>true</code> by default</li>
		 *     <li>"headers": The expected request headers</li>
		 *     <li>"method": The expected HTTP method; "GET" by default</li>
		 *   </ul>
		 *   A string is interpreted as URL with method "GET". Spaces inside the URL, and "'" and
		 *   "~" inside the query string are percent-encoded automatically.
		 * @param {object|Promise|Error} [oResponse] The response message to be returned from the
		 *   requestor or a promise on it
		 * @param {object} [mResponseHeaders] The response headers to be returned from the
		 *   requestor
		 * @returns {object} The test instance for chaining
		 */
		expectRequest : function (vRequest, oResponse, mResponseHeaders) {
			var aUrlParts;

			if (typeof vRequest === "string") {
				vRequest = {
					deepPath : "/" + vRequest.split("?")[0],
					method : "GET",
					requestUri : vRequest
				};
			}
			// ensure that these properties are defined (required for deepEqual)
			vRequest.headers = vRequest.headers || {};
			vRequest.method = vRequest.method || "GET";
			vRequest.responseHeaders = mResponseHeaders || {};
			vRequest.response = oResponse || {/*null object pattern*/};
			aUrlParts = vRequest.requestUri.split("?");
			if (aUrlParts[1] && vRequest.encodeRequestUri !== false) {
				vRequest.requestUri = aUrlParts[0] + "?"
					+ aUrlParts[1].replace(/ /g, "%20").replace(/'/g, "%27").replace(/~/g, "%7e");
			}
			delete vRequest.encodeRequestUri;
			this.aRequests.push(vRequest);

			return this;
		},

		/**
		 * Returns whether expected changes for the control are only optional null values.
		 *
		 * @param {string} sControlId The control ID
		 * @returns {boolean} Whether expected changes for the control are only optional null values
		 */
		hasOnlyOptionalChanges : function (sControlId) {
			return this.bNullOptional &&
				this.mChanges[sControlId].every(function (vValue) {
					return vValue === null;
				});
		},

		/**
		 * Allows that the property "text" of the control with the given ID is set to undefined or
		 * null. This may happen when bindings are initialized before the model value is available.
		 *
		 * @param {string} sControlId The control ID
		 * @returns {object} The test instance for chaining
		 */
		ignoreNullChanges : function (sControlId) {
			this.mIgnoredChanges[sControlId] = true;

			return this;
		},

		/**
		 * Observes and checks value changes for a control. In case the test uses {#expectChange},
		 * checks the model internal value by attaching a formatter; if the test uses
		 * {#expectValue}, checks the control value in its external representation using a managed
		 * object observer. In both cases, {#checkValue} is called for the actual value check each
		 * time the value changes.
		 * Note that you may only use controls that have a 'text' or a 'value' property.
		 * Note that the managed object observer for list changes only supports sap.ui.table.Table
		 * with one column currently; this can be enhanced as needed.
		 *
		 * @param {object} assert The QUnit assert object
		 * @param {sap.ui.base.ManagedObject} oControl The control
		 * @param {string} sControlId The (symbolic) control ID for which changes are expected
		 * @param {boolean} [bInList] Whether the control resides in a list item
		 */
		observe : function (assert, oControl, sControlId, bInList) {
			var oBindingInfo,
				fnOriginalFormatter,
				sProperty = oControl.getBindingInfo("text") ? "text" : "value",
				oType,
				bIsCompositeType,
				that = this;

			if (this.bCheckValue) { // ManagedObjectObserver checks value changes on the control
				this.oObserver = this.oObserver || new ManagedObjectObserver(function (oChange) {
					that.checkValue(assert, oChange.current, sControlId,
						/*TODO only for sap.ui.table.Row*/ oChange.object.getParent().getIndex());
				});
				if (bInList) {
					this.oTemplateObserver = this.oTemplateObserver
						|| new ManagedObjectObserver(function (oChange) {
						var oCellControl;

						if (oChange.mutation === "remove") {
							that.oObserver.unobserve(oChange.child);
						} else if (oChange.mutation === "insert") {
							//TODO find cell with observed control, currently assume 0
							//TODO aggregation "cells" is only valid for sap.ui.table.Table
							oCellControl = oChange.child.getAggregation("cells")[0];
							that.checkValue(assert, oCellControl.getProperty(sProperty), sControlId,
								oChange.child.getIndex());
							that.oObserver.observe(oCellControl, {properties : [sProperty]});
						}
					});
					//TODO aggregation "rows" is only valid for sap.ui.table.Table
					this.oTemplateObserver.observe(oControl.getParent().getParent(),
						{aggregations : ["rows"]});
				} else {
					this.oObserver.observe(oControl, {properties : [sProperty]});
				}
				return;
			}
			// formatter checks changes of values in model representation
			oBindingInfo = oControl.getBindingInfo(sProperty);
			fnOriginalFormatter = oBindingInfo.formatter;
			oType = oBindingInfo.type;
			bIsCompositeType = oType && oType.getMetadata().isA("sap.ui.model.CompositeType");
			oBindingInfo.formatter = function (sValue) {
				var oContext = bInList && this.getBindingContext();

				if (fnOriginalFormatter) {
					sValue = fnOriginalFormatter.apply(this, arguments);
				} else if (bIsCompositeType) {
					// composite type at binding with type and no original formatter: call the
					// type's formatValue, as CompositeBinding#getExternalValue calls only the
					// formatter if it is set
					sValue = oType.formatValue.call(oType, Array.prototype.slice.call(arguments),
						"string");
				}
				// CompositeType#formatValue is called each time a part changes; we expect null if
				// not all parts are set as it is the case for sap.ui.model.odata.type.Unit.
				// Only check the value once all parts are available.
				if (!bIsCompositeType || sValue !== null) {
					that.checkValue(assert, sValue, sControlId,
						oContext && (oContext.getBinding
							? oContext.getBinding() && oContext.getIndex()
							: oContext.getPath()
						)
					);
				}

				return sValue;
			};
		},

		/**
		 * Waits for the expected requests and changes.
		 *
		 * @param {object} assert The QUnit assert object
		 * @param {boolean} [bNullOptional] Whether a non-list change to a null value is optional
		 * @param {number} [iTimeout=3000] The timeout time in milliseconds
		 * @returns {Promise} A promise that is resolved when all requests have been responded and
		 *   all expected values for controls have been set
		 */
		waitForChanges : function (assert, bNullOptional, iTimeout) {
			var oPromise,
				that = this;

			oPromise = new SyncPromise(function (resolve) {
				that.resolve = resolve;
				that.bNullOptional = bNullOptional;
				// After three seconds everything should have run through
				// Resolve to have the missing requests and changes reported
				setTimeout(function () {
					if (oPromise.isPending()) {
						assert.ok(false, "Timeout in waitForChanges");
						resolve();
					}
				}, iTimeout || 3000);
				that.checkFinish(assert);
			}).then(function () {
				var sControlId, aExpectedValuesPerRow, i, j;

				// Report missing requests
				that.aRequests.forEach(function (oRequest) {
					assert.ok(false, oRequest.method + " " + oRequest.requestUri
						+ " (not requested)");
				});
				// Report missing changes
				for (sControlId in that.mChanges) {
					if (that.hasOnlyOptionalChanges(sControlId)) {
						delete that.mChanges[sControlId];
						continue;
					}
					for (i in that.mChanges[sControlId]) {
						assert.ok(false, sControlId + ": "
							+ JSON.stringify(that.mChanges[sControlId][i]) + " (not set)");
					}
				}
				for (sControlId in that.mListChanges) {
					// Note: This may be a sparse array
					aExpectedValuesPerRow = that.mListChanges[sControlId];
					for (i in aExpectedValuesPerRow) {
						for (j in aExpectedValuesPerRow[i]) {
							assert.ok(false, sControlId + "[" + i + "]: "
								+ JSON.stringify(aExpectedValuesPerRow[i][j]) + " (not set)");
						}
					}
				}
				that.checkMessages(assert);
			});

			return oPromise;
		}
	});

	/**
	 *
	 * Creates a test with the given title and executes viewStart with the given parameters.
	 *
	 * @param {string} sTitle The title of the test case
	 * @param {string} sView The XML snippet of the view
	 * @param {object} mResponseByRequest A map containing the request as key
	 *   and response as value
	 * @param {object|object[]} mValueByControl A map or an array of maps containing control id as
	 *   key and the expected control values as value
	 * @param {string|sap.ui.model.odata.v4.ODataModel} [vModel]
	 *   The model (or the name of a function at <code>this</code> which creates it); it is attached
	 *   to the view and to the test instance.
	 *   If no model is given, the <code>GWSAMPLE_BASIC</code> model is created and used.
	 * @param {function} [fnAssert]
	 *   A function containing additional assertions such as expected log messages which is called
	 *   just before view creation with the test as "this"
	 */
	function testViewStart(sTitle, sView, mResponseByRequest, mValueByControl, vModel, fnAssert) {

		QUnit.test(sTitle, function (assert) {
			var sControlId, sRequest, that = this;

			function expectChanges(mValueByControl) {
				for (sControlId in mValueByControl) {
					that.expectChange(sControlId, mValueByControl[sControlId]);
				}
			}

			for (sRequest in mResponseByRequest) {
				this.expectRequest(sRequest, mResponseByRequest[sRequest]);
			}
			if (Array.isArray(mValueByControl)) {
				mValueByControl.forEach(expectChanges);
			} else {
				expectChanges(mValueByControl);
			}
			if (typeof vModel === "string") {
				vModel = this[vModel]();
			}
			if (fnAssert) {
				fnAssert.call(this);
			}

			return this.createView(assert, sView, vModel);
		});
	}

	//*********************************************************************************************
	// Integration test for correct path calculation during ODataModel#read(...).
	// JIRA: CPOUI5MODELS-404
	// BCP: 2080464216
[{
	expectedCanonicalRequest : "SalesOrderSet",
	expectedRequest : "SalesOrderSet",
	isArrayResponse : true,
	path : "/SalesOrderSet",
	title : "Absolute path with one segment to a collection"
}, {
	expectedCanonicalRequest : "SalesOrderSet('1')",
	expectedRequest : "SalesOrderSet('1')",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')",
	title : "Absolute path with one segment to a single entity"
}, {
	expectedCanonicalRequest : "SalesOrder_Confirm(SalesOrderID='1')",
	expectedRequest : "SalesOrder_Confirm(SalesOrderID='1')",
	isArrayResponse : false,
	path : "/SalesOrder_Confirm(SalesOrderID='1')",
	title : "Function import"
}, {
	expectedCanonicalRequest : "SalesOrderSet('1')/ToLineItems",
	expectedRequest : "SalesOrderSet('1')/ToLineItems",
	isArrayResponse : true,
	path : "/SalesOrderSet('1')/ToLineItems",
	title : "Absolute path with two segments to a collection"
}, {
	// MockServer does not support navigation properties with key predicates; as long as the
	// addressed entity set is addressable we can shorten the path
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')",
		requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"
	},
	expectedRequest : "SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')",
	title : "Absolute path with two segments to a single entity of a collection"
}, {
	expectedCanonicalRequest : "SalesOrderSet('1')/ToBusinessPartner",
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToBusinessPartner",
	title : "Absolute path with two segments to a single entity via 'to 1' navigation property"
}, {
	expectedCanonicalRequest : "BusinessPartnerSet('BP1')/Address",
	expectedRequest : "BusinessPartnerSet('BP1')/Address",
	isArrayResponse : false,
	path : "/BusinessPartnerSet('BP1')/Address",
	title : "Absolute path with two segments to a complex type"
}, {
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/ToProduct",
		requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
	},
	expectedRequest : "SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')"
		+ "/ToProduct",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/ToProduct",
	title : "Absolute path with three segments to a single entity; 'to n' navigation in the"
		+ " middle"
}, {
	expectedCanonicalRequest : {
		deepPath : "/BusinessPartnerSet('BP1')/ToProducts('P1')/ToSalesOrderLineItems",
		requestUri : "ProductSet('P1')/ToSalesOrderLineItems"
	},
	expectedRequest : "BusinessPartnerSet('BP1')/ToProducts('P1')/ToSalesOrderLineItems",
	isArrayResponse : true,
	path : "/BusinessPartnerSet('BP1')/ToProducts('P1')/ToSalesOrderLineItems",
	title : "Absolute path with three segments to a collection; 'to n' navigation in the middle"
}, {
	expectedCanonicalRequest : {
		deepPath : "/BusinessPartnerSet('BP1')/ToProducts('P1')"
			+ "/ToSalesOrderLineItems(SalesOrderID='1',ItemPosition='10')",
		// MockServer does not support navigation properties with key predicates; as long as the
		// addressed entity set is addressable we can shorten the path
		requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"
	},
	expectedRequest : "BusinessPartnerSet('BP1')/ToProducts('P1')"
		+ "/ToSalesOrderLineItems(SalesOrderID='1',ItemPosition='10')",
	isArrayResponse : false,
	path : "/BusinessPartnerSet('BP1')/ToProducts('P1')"
		+ "/ToSalesOrderLineItems(SalesOrderID='1',ItemPosition='10')",
	title : "Absolute path with three segments to a single entity of a collection; 'to n'"
		+ " navigation in the middle"
}, {
	// path cannot be made canonical as the key predicate for the referenced product is missing
	expectedCanonicalRequest : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
		+ "/ToSupplier",
	expectedRequest : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
		+ "/ToSupplier",
	isArrayResponse : false,
	path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct/ToSupplier",
	title : "Absolute path with three segments to a single entity; 'to 1' navigation in the"
		+ " middle"
}, {
	// path cannot be made canonical as the key predicate for the business partner is missing
	expectedCanonicalRequest : "SalesOrderSet('1')/ToBusinessPartner/ToProducts",
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner/ToProducts",
	isArrayResponse : true,
	path : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts",
	title : "Absolute path with three segments to a collection; 'to 1' navigation in the middle"
}, {
	// even if key predicates for ToBusinessPartner cannot be resolved, canonical path for the
	// product can be computed
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
		requestUri : "ProductSet('P1')"
	},
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
	title : "Absolute path with three segments to a single entity of a collection; 'to 1'"
		+ " navigation in the middle"
}, {
	expectedCanonicalRequest : "SalesOrderSet('1')/ToBusinessPartner/Address",
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner/Address",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToBusinessPartner/Address",
	title : "Absolute path with three segments to a complex type; 'to 1' navigation in the"
		+ " middle"
}, {
	expectedCanonicalRequest : "SalesOrderSet/$count",
	expectedRequest : "SalesOrderSet/$count",
	isArrayResponse : false,
	path : "/SalesOrderSet/$count",
	title : "Absolute path; second segment is $count"
}, {
	expectedCanonicalRequest : "SalesOrderSet('1')/ToLineItems/$count",
	expectedRequest : "SalesOrderSet('1')/ToLineItems/$count",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToLineItems/$count",
	title : "Absolute path; third segment is $count; 'to n' navigation in the middle"
}, {
	expectedCanonicalRequest : {
		deepPath : "/BusinessPartnerSet('BP1')/ToProducts('P1')/ToSalesOrderLineItems/$count",
		requestUri : "ProductSet('P1')/ToSalesOrderLineItems/$count"
	},
	expectedRequest : "BusinessPartnerSet('BP1')/ToProducts('P1')/ToSalesOrderLineItems/$count",
	isArrayResponse : false,
	path : "/BusinessPartnerSet('BP1')/ToProducts('P1')/ToSalesOrderLineItems/$count",
	title : "Absolute path; 4th segment is $count"
}, {
	contextDeepPath : "/SalesOrderSet",
	contextPath : "/SalesOrderSet",
	expectedCanonicalRequest : "SalesOrderSet",
	expectedRequest : "SalesOrderSet",
	isArrayResponse : true,
	path : "",
	title : "Relative empty path; resolved path has 1 segment referencing a collection"
}, {
	contextDeepPath : "/SalesOrderSet('1')",
	contextPath : "/SalesOrderSet('1')",
	expectedCanonicalRequest : "SalesOrderSet('1')",
	expectedRequest : "SalesOrderSet('1')",
	isArrayResponse : false,
	path : "",
	title : "Relative empty path; resolved path has 1 segment referencing a single entity"
}].forEach(function (oFixture) {
	[false, true].forEach(function (bCanonical) {
	var sTitle = "ODataModel#read:" + oFixture.title
			+ (bCanonical ? "; using canonical requests" : "");

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModel({tokenHandling : false, useBatch : true}),
			that = this;

		return this.createView(assert, "", oModel).then(function () {
			var oContext = oFixture.contextPath
					? oModel.getContext(oFixture.contextPath, oFixture.contextDeepPath)
					: undefined,
				vExpectedRequest = bCanonical
					? oFixture.expectedCanonicalRequest
					: oFixture.expectedRequest,
				mParameters = {canonicalRequest : bCanonical, context : oContext};

			if (oFixture.pathCache) {
				oModel.mPathCache = oFixture.pathCache;
			}
			that.expectRequest(vExpectedRequest,
				// response not relevant for this test
				oFixture.isArrayResponse ? {results : []} : {});

			// code under test
			oModel.read(oFixture.path, mParameters);

			return that.waitForChanges(assert);
		});
	});
	});
});

	//*********************************************************************************************
	// Integration test for correct path calculation during ODataModel#read(...). A previous read
	// filled already the path cache, that "to 1" navigation properties can be resolved.
	// JIRA: CPOUI5MODELS-404
	// BCP: 2080464216
[{
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
			+ "/ToSupplier",
		requestUri : "ProductSet('P1')/ToSupplier"
	},
	expectedRequest : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
		+ "/ToSupplier",
	isArrayResponse : false,
	path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct/ToSupplier",
	previousReads : [{
		request : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct",
		response : {__metadata : {uri : "ProductSet('P1')"} /*content not relevant*/}
	}, {
		request : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct/ToSupplier",
		response : {__metadata : {uri : "BusinessPartnerSet('BP1')"} /*content not relevant*/}
	}],
	title : "Absolute path with three segments to a single entity; 'to 1' navigation in the"
		+ " middle"
}, {
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts",
		requestUri : "BusinessPartnerSet('BP1')/ToProducts"
	},
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner/ToProducts",
	isArrayResponse : true,
	path : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts",
	previousReads : [{
		request : "SalesOrderSet('1')/ToBusinessPartner",
		response : {__metadata : {uri : "BusinessPartnerSet('BP1')"} /*content not relevant*/}
	}],
	title : "Absolute path with three segments to a collection; 'to 1' navigation in the middle"
}, {
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
		// MockServer does not support navigation properties with key predicates; as long as the
		// addressed entity set is addressable we can shorten the path
		requestUri : "ProductSet('P1')"
	},
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
	previousReads : [{
		request : "SalesOrderSet('1')/ToBusinessPartner",
		response : {__metadata : {uri : "BusinessPartnerSet('BP1')"} /*content not relevant*/}
	}, {
		request : "SalesOrderSet('1')/ToBusinessPartner/ToProducts('P1')",
		response : {__metadata : {uri : "ProductSet('P1')"} /*content not relevant*/}
	}],
	title : "Absolute path with three segments to a single entity of a collection; 'to 1'"
		+ " navigation in the middle"
}, {
	expectedCanonicalRequest : "SalesOrderSet('1')/ToBusinessPartner/Address",
	expectedRequest : "SalesOrderSet('1')/ToBusinessPartner/Address",
	isArrayResponse : false,
	path : "/SalesOrderSet('1')/ToBusinessPartner/Address",
	previousReads : [{
		request : "SalesOrderSet('1')/ToBusinessPartner",
		response : {__metadata : {uri : "BusinessPartnerSet('BP1')"} /*content not relevant*/}
	}],
	title : "Absolute path with three segments to a complex type; 'to 1' navigation in the"
		+ " middle"
}, {
	contextDeepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct",
	contextPath : "/ProductSet('P1')",
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
			+ "/ToSupplier",
		requestUri : "ProductSet('P1')/ToSupplier"
	},
	expectedRequest : {
		deepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
			+ "/ToSupplier",
		requestUri : "ProductSet('P1')/ToSupplier"
	},
	isArrayResponse : false,
	path : "ToSupplier",
	previousReads : [{
		request : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct",
		response : {__metadata : {uri : "ProductSet('P1')"} /*content not relevant*/}
	}, {
		request : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct/ToSupplier",
		response : {__metadata : {uri : "BusinessPartnerSet('BP1')"} /*content not relevant*/}
	}],
	title : "Relative path 'ToSupplier'; resolved deep path has 3 segments"
}, {
	contextDeepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
		+ "/ToSupplier",
	contextPath : "/ProductSet('P1')/ToSupplier",
	expectedCanonicalRequest : {
		deepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
			+ "/ToSupplier",
		requestUri : "ProductSet('P1')/ToSupplier"
	},
	expectedRequest : {
		deepPath : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct"
			+ "/ToSupplier",
		requestUri : "ProductSet('P1')/ToSupplier"
	},
	isArrayResponse : false,
	path : "",
	previousReads : [{
		request : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct",
		response : {__metadata : {uri : "ProductSet('P1')"} /*content not relevant*/}
	}, {
		request : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct/ToSupplier",
		response : {__metadata : {uri : "BusinessPartnerSet('BP1')"} /*content not relevant*/}
	}],
	title : "Relative empty path with '/ProductSet('P1')/ToSupplier' as context path; resolved"
		+ " deep path has 3 segments"
}].forEach(function (oFixture) {
	[false, true].forEach(function (bCanonical) {
	var sTitle = "ODataModel#read:" + oFixture.title
			+ (bCanonical ? "; using canonical requests" : "")
			+ "; 'to 1' navigation property in the middle already read";

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModel({tokenHandling : false, useBatch : true}),
			that = this;

		oFixture.previousReads.forEach(function (oPreviousRead) {
			that.expectRequest(oPreviousRead.request, oPreviousRead.response);

			// trigger previous read to be able to resolve the "to 1" navigation property
			oModel.read("/" + oPreviousRead.request);
		});

		return this.createView(assert, "", oModel).then(function () {
			var oContext = oFixture.contextPath
					? oModel.getContext(oFixture.contextPath, oFixture.contextDeepPath)
					: undefined,
				vExpectedRequest = bCanonical
					? oFixture.expectedCanonicalRequest
					: oFixture.expectedRequest,
				mParameters = {canonicalRequest : bCanonical, context : oContext};

			that.expectRequest(vExpectedRequest,
				// response not relevant for this test
				oFixture.isArrayResponse ? {results : []} : {});

			// code under test
			oModel.read(oFixture.path, mParameters);

			return that.waitForChanges(assert);
		});
	});
	});
});

	//*********************************************************************************************
	// Integration test for correct path calculation during ODataModel#read(...). "To n" navigation
	// properties are not shortened if the corresponding entity set is not addressable.
	// JIRA: CPOUI5MODELS-404
	// BCP: 2080464216
	QUnit.test("ODataModel#read: not addressable 'to n' navigation property" , function (assert) {
		var oModel = createSpecialCasesModel({tokenHandling : false, useBatch : true}),
			sResourcePath = "C_SubscrpnProductChargeTP('ID')/to_AllUserContactCards",
			that = this;

		this.expectRequest(sResourcePath, {
			results : [{__metadata : {uri : "I_UserContactCard('Card1')"}}]
		});

		// trigger previous read to be able to resolve the "to 1" navigation property
		oModel.read("/" + sResourcePath);

		return this.createView(assert, "", oModel).then(function () {
			that.expectRequest(sResourcePath + "('Card1')", {});

			// code under test
			oModel.read("/" + sResourcePath + "('Card1')", {canonicalRequest : true});

			return that.waitForChanges(assert);
		});
	});


	//*********************************************************************************************
	// Integration test for correct path calculation during ODataModel#read(...). "To 1" navigation
	// properties are not shortened independent whether the corresponding entity set is addressable.
	// JIRA: CPOUI5MODELS-404
	// BCP: 2080464216
	QUnit.test("ODataModel#read: not addressable 'to 1' navigation property" , function (assert) {
		var oModel = createSpecialCasesModel({tokenHandling : false, useBatch : true}),
			sResourcePath = "C_SubscrpnProductChargeTP('ID')/to_CreatedByUserContactCard",
			that = this;

		this.expectRequest(sResourcePath, {__metadata : {uri : "I_UserContactCard('Card1')"}});

		// trigger previous read to be able to resolve the "to 1" navigation property
		oModel.read("/" + sResourcePath);

		return this.createView(assert, "", oModel).then(function () {
			that.expectRequest(sResourcePath, {});

			// code under test
			oModel.read("/" + sResourcePath, {canonicalRequest : true});

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Integration test for correct path calculation during ODataModel#read(...). If the given path
	// has a query string, the query string is ignored by ODataModel#read.
	// JIRA: CPOUI5MODELS-404
	// BCP: 2080464216
[{
	path : "/SalesOrderSet('1')/ToBusinessPartner?sap-client=100"
}, {
	contextPath : "/SalesOrderSet('1')/ToBusinessPartner",
	path : "?sap-client=100"
}].forEach(function (oFixture) {
	QUnit.test("ODataModel#read: path with query string: " + oFixture.path, function (assert) {
		var oModel = createSalesOrdersModel({tokenHandling : false, useBatch : true}),
			that = this;

		return this.createView(assert, "", oModel).then(function () {
			that.expectRequest("SalesOrderSet('1')/ToBusinessPartner",
				// response not relevant for this test
				{results : {}});

			// code under test
			oModel.read(oFixture.path, oFixture.contextPath
				? {context : oModel.getContext(oFixture.contextPath)}
				: {});

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Read and display data for a single field in a form
	// Usage of service: /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/
	testViewStart("Minimal integration test", '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
</FlexBox>',
		{"SalesOrderSet('1')" : {SalesOrderID : "1"}},
		[{id : null}, {id : "1"}]
	);

	//*********************************************************************************************
	// Scenario: Read and display data for a single field in a form
	// Usage of service: /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/
	QUnit.test("Minimal integration test (useBatch=true)", function (assert) {
		var oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
</FlexBox>';

		this.expectHeadRequest()
			.expectRequest("SalesOrderSet('1')", {
				SalesOrderID : "1"
			})
			.expectChange("id", null)
			.expectChange("id", "1");

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: Read and display collection data for a table with a single field
	// Usage of service: /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/
	QUnit.test("Minimal integration test with collection data", function (assert) {
		var sView = '\
<Table id="table" items="{/SalesOrderSet}">\
	<Text id="id" text="{SalesOrderID}" />\
</Table>';

		this.expectRequest("SalesOrderSet?$skip=0&$top=100", {
				results : [
					{SalesOrderID : "0500000001"},
					{SalesOrderID : "0500000002"}
				]
			})
			.expectChange("id", ["0500000001", "0500000002"]);

		// code under test
		return this.createView(assert, sView);
	});

	//*********************************************************************************************
	// Scenario: Read and display collection data for a table with a single field
	// Usage of service: /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/
	QUnit.test("Minimal integration test with collection data (useBatch=true)", function (assert) {
		var oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<Table id="table" items="{/SalesOrderSet}">\
	<Text id="id" text="{SalesOrderID}" />\
</Table>';

		this.expectHeadRequest()
			.expectRequest("SalesOrderSet?$skip=0&$top=100", {
				results : [
					{SalesOrderID : "0500000001"},
					{SalesOrderID : "0500000002"}
				]
			})
			.expectChange("id", ["0500000001", "0500000002"]);

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: A single request within a $batch request fails
	// JIRA: CPOUI5MODELS-198
	QUnit.test("$batch error handling: single request fails", function (assert) {
		var oErrorResponse = createErrorResponse({message : "Bad Request", statusCode : 400}),
			oEventHandlers = {
				batchCompleted : function () {},
				batchFailed : function () {},
				batchSent : function () {},
				requestCompleted : function () {},
				requestFailed : function () {},
				requestSent : function () {}
			},
			oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
</FlexBox>\
<Table id="table" items="{/SalesOrderSet}">\
	<Text text="{SalesOrderID}" />\
</Table>';

		this.expectHeadRequest()
			.expectRequest("SalesOrderSet('1')", {
				SalesOrderID : "1"
			})
			.expectChange("id", null)
			.expectChange("id", "1")
			.expectRequest("SalesOrderSet?$skip=0&$top=100", oErrorResponse)
			.expectMessages([{
				code : "UF0",
				descriptionUrl : "",
				fullTarget : "/SalesOrderSet",
				message : "Bad Request",
				persistent : false,
				target : "/SalesOrderSet",
				technical : true,
				type : "Error"
			}]);

		// don't care about passed arguments
		this.mock(oEventHandlers).expects("batchCompleted");
		this.mock(oEventHandlers).expects("batchFailed").never();
		this.mock(oEventHandlers).expects("batchSent");
		this.mock(oEventHandlers).expects("requestCompleted").twice();
		this.mock(oEventHandlers).expects("requestFailed");
		this.mock(oEventHandlers).expects("requestSent").twice();
		oModel.attachBatchRequestCompleted(oEventHandlers.batchCompleted);
		oModel.attachBatchRequestFailed(oEventHandlers.batchFailed);
		oModel.attachBatchRequestSent(oEventHandlers.batchSent);
		oModel.attachRequestCompleted(oEventHandlers.requestCompleted);
		oModel.attachRequestFailed(oEventHandlers.requestFailed);
		oModel.attachRequestSent(oEventHandlers.requestSent);
		this.oLogMock.expects("error")
			.withExactArgs("'HTTP request failed' while processing "
					+ "GET SalesOrderSet?$skip=0&$top=100",
				JSON.stringify(oErrorResponse), sODataModelClassName);

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: If network connection is lost, browsers may send status code 0; in that case a
	// generic, technical message is added to the message model.
	QUnit.test("$batch error handling: no network connection - generic error", function (assert) {
		var oEventHandlers = {
				batchCompleted : function () {},
				batchFailed : function () {},
				batchSent : function () {},
				requestCompleted : function () {},
				requestFailed : function () {},
				requestSent : function () {}
			},
			oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
</FlexBox>\
<Table id="table" items="{/SalesOrderSet}">\
	<Text text="{SalesOrderID}" />\
</Table>';

		this.mock(sap.ui.getCore().getLibraryResourceBundle()).expects("getText")
			.atLeast(1)
			.callsFake(function (sKey) {
				return sKey;
			});
		this.expectHeadRequest()
			.expectRequest("SalesOrderSet('1')", {
				body : "",
				crashBatch : true,
				headers : [],
				statusCode : 0,
				statusText : ""
			})
			.expectRequest("SalesOrderSet?$skip=0&$top=100" /* response not relevant */)
			.expectMessages([{
				code : "",
				description : "",
				message : "CommunicationError",
				persistent : true,
				target : "",
				technical : true,
				type : "Error"
			}]);

		// don't care about passed arguments
		this.mock(oEventHandlers).expects("batchCompleted");
		this.mock(oEventHandlers).expects("batchFailed");
		this.mock(oEventHandlers).expects("batchSent");
		this.mock(oEventHandlers).expects("requestCompleted").twice();
		this.mock(oEventHandlers).expects("requestFailed").twice();
		this.mock(oEventHandlers).expects("requestSent").twice();
		oModel.attachBatchRequestCompleted(oEventHandlers.batchCompleted);
		oModel.attachBatchRequestFailed(oEventHandlers.batchFailed);
		oModel.attachBatchRequestSent(oEventHandlers.batchSent);
		oModel.attachRequestCompleted(oEventHandlers.requestCompleted);
		oModel.attachRequestFailed(oEventHandlers.requestFailed);
		oModel.attachRequestSent(oEventHandlers.requestSent);
		this.oLogMock.expects("error")
			.withExactArgs("'HTTP request failed' while processing "
					+ "POST /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/$batch",
				JSON.stringify({body : "", headers : [], statusCode : 0, statusText : ""}),
				sODataModelClassName);

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: A single request caused complete $batch to fail
	// JIRA: CPOUI5MODELS-198
	QUnit.test("$batch error handling: complete batch fails", function (assert) {
		var oErrorResponse = createErrorResponse({crashBatch : true}),
			oEventHandlers = {
				batchCompleted : function () {},
				batchFailed : function () {},
				batchSent : function () {},
				requestCompleted : function () {},
				requestFailed : function () {},
				requestSent : function () {}
			},
			oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<Table id="table" items="{/SalesOrderSet}">\
	<Text text="{SalesOrderID}" />\
</Table>\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
</FlexBox>';

		this.expectHeadRequest()
			.expectRequest("SalesOrderSet?$skip=0&$top=100", oErrorResponse)
			.expectRequest("SalesOrderSet('1')", {
				SalesOrderID : "1"
			})
			.expectMessages([{
				code : "UF0",
				descriptionUrl : "",
				fullTarget : "/$batch",
				message : "Internal Server Error",
				persistent : false,
				target : "/$batch",
				technical : true,
				type : "Error"
			}])
			.expectChange("id", null); // no change as batch fails

		// don't care about passed arguments
		this.mock(oEventHandlers).expects("batchCompleted");
		this.mock(oEventHandlers).expects("batchFailed");
		this.mock(oEventHandlers).expects("batchSent");
		this.mock(oEventHandlers).expects("requestCompleted").twice();
		this.mock(oEventHandlers).expects("requestFailed").twice();
		this.mock(oEventHandlers).expects("requestSent").twice();
		oModel.attachBatchRequestCompleted(oEventHandlers.batchCompleted);
		oModel.attachBatchRequestFailed(oEventHandlers.batchFailed);
		oModel.attachBatchRequestSent(oEventHandlers.batchSent);
		oModel.attachRequestCompleted(oEventHandlers.requestCompleted);
		oModel.attachRequestFailed(oEventHandlers.requestFailed);
		oModel.attachRequestSent(oEventHandlers.requestSent);
		this.oLogMock.expects("error")
			.withExactArgs("'HTTP request failed' while processing "
					+ "POST /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/$batch",
				JSON.stringify(createErrorResponse()), sODataModelClassName);

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: Complete $batch fails with a technical error and the response has Content-Type
	// "text/plain": A persistent, generic UI message is created to show the issue on the UI.
	// BCP: 002075129500003079342020
	QUnit.test("$batch error handling: complete batch fails, plain error", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			sView = '\
<Table id="table" items="{/SalesOrderSet}">\
	<Text text="{SalesOrderID}" />\
</Table>';

		this.mock(sap.ui.getCore().getLibraryResourceBundle()).expects("getText")
			.atLeast(1)
			.callsFake(function (sKey, aArgs) {
				return sKey;
			});
		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet?$skip=0&$top=100"
			}, {
				body : "A plain error text",
				crashBatch : true,
				headers : {
					"Content-Type" : "text/plain;charset=utf-8"
				},
				statusCode : 503
			})
			.expectMessages([{
				code : "",
				description : "A plain error text",
				descriptionUrl : "",
				fullTarget : "",
				message : "CommunicationError",
				persistent : true,
				target : "",
				technical : true,
				type : "Error"
			}]);

		this.oLogMock.expects("error")
			.withExactArgs("'HTTP request failed' while processing POST /SalesOrderSrv/$batch",
				JSON.stringify({
					body : "A plain error text",
					headers : {
						"Content-Type" : "text/plain;charset=utf-8"
					},
					statusCode : 503
				}), sODataModelClassName);
		this.oLogMock.expects("error")
			.withExactArgs("Error message returned by server could not be parsed");

		oModel.setMessageScope(MessageScope.BusinessObject);

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: Message with empty target (tested as single request and as batch request)
[false, true].forEach(function (bUseBatch) {
	QUnit.test("Messages: empty target (useBatch=" + bUseBatch + ")", function (assert) {
		var oModel = createSalesOrdersModel({useBatch : bUseBatch}),
			oResponseMessage = this.createResponseMessage(""),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
</FlexBox>';

		if (bUseBatch) {
			this.expectHeadRequest();
		}

		this.expectRequest("SalesOrderSet('1')", {SalesOrderID : "1"},
				{"sap-message" : getMessageHeader(oResponseMessage)})
			.expectChange("id", null)
			.expectChange("id", "1")
			.expectMessage(oResponseMessage, "/SalesOrderSet('1')");

		// code under test
		return this.createView(assert, sView, oModel);
	});
});

	//*********************************************************************************************
	// Scenario: Message with a simple target in a complex data type
	// JIRA: CPOUI5MODELS-35
	QUnit.test("Messages: simple target with complex data type", function (assert) {
		var oModel = createSalesOrdersModel(),
			oResponseMessage = this.createResponseMessage("Address/City", "Foo"),
			sView = '\
<FlexBox binding="{/BusinessPartnerSet(\'1\')}">\
	<Text id="CompanyName" text="{CompanyName}" />\
	<Input id="City" value="{Address/City}" />\
</FlexBox>',
			that = this;

		this.expectRequest("BusinessPartnerSet('1')", {
				CompanyName : "SAP SE",
				Address : {
					City : "Walldorf"
				}
			}, {"sap-message" : getMessageHeader(oResponseMessage)})
			.expectChange("CompanyName", null)
			.expectChange("CompanyName", "SAP SE")
			.expectChange("City", null)
			.expectChange("City", "Walldorf")
			.expectMessage(oResponseMessage,"/BusinessPartnerSet('1')/");

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			return that.checkValueState(assert, "City", "Error", "Foo");
		});
	});

	//*********************************************************************************************
	// Scenario: Messages with a http status code of 4xx/5xx are expected to be technical
	// JIRA: CPOUI5MODELS-103
[
	{message : "Bad Request", statusCode : 400},
	{message : "Internal Server Error", statusCode : 500}
].forEach(function (oFixture) {
	var sTitle = "Messages: http status code '" + oFixture.statusCode + "' expects a technical "
			+ "error message";

	QUnit.test(sTitle , function (assert) {
		var oModel = createSalesOrdersModel(),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text text="{SalesOrderID}" />\
</FlexBox>';

		this.oLogMock.expects("error").once();

		this.expectRequest("SalesOrderSet('1')", createErrorResponse(oFixture))
			.expectMessages([{
				code : "UF0",
				fullTarget : "/SalesOrderSet('1')",
				message : oFixture.message,
				persistent : false,
				technical : true,
				target : "/SalesOrderSet('1')",
				type : MessageType.Error
			}]);

		// code under test
		return this.createView(assert, sView, oModel);
	});
});

	//*********************************************************************************************
	// Scenario: Error responses that contain messages within the response body (technical messages)
	// will not process any messages if the http status code is <400.
	// JIRA: CPOUI5MODELS-103
	QUnit.test("Messages: messages within a response body are not processed if http status code is "
			+ "'200'", function (assert) {
		var oModel = createSalesOrdersModel(),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text text="{SalesOrderID}" />\
</FlexBox>';

		this.expectRequest("SalesOrderSet('1')", createErrorResponse({statusCode : 200}))
			.expectMessages([]); // clean all expected messages

		// code under test
		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: Messages are bound against targets with a deeper path (more than one navigation
	// property). Tested with simple type (productName) and complex type (supplierAddress).
	// JIRA: CPOUI5MODELS-103
	QUnit.test("Messages: more than one navigation property", function (assert) {
		var oModel = createSalesOrdersModel(),
			oMsgProductName = this.createResponseMessage("Name", "Foo"),
			oMsgSupplierAddress = this.createResponseMessage("Address/City", "Bar", "warning"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderId" text="{SalesOrderID}" />\
	<Table id="table" items="{ToLineItems}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
	</Table>\
</FlexBox>\
<FlexBox id="detailProduct" binding="{ToProduct}">\
	<Input id="productName" value="{Name}" />\
</FlexBox>\
<FlexBox id="detailSupplier" binding="{ToSupplier}">\
	<Input id="supplierAddress" value="{Address/City}" />\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", {
				SalesOrderID : "1"
			})
			.expectRequest("SalesOrderSet('1')/ToLineItems?$skip=0&$top=100", {
				results : [{
					__metadata : {
						uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					SalesOrderID : "1",
					ItemPosition : "10"
				}, {
					__metadata : {
						uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					SalesOrderID : "1",
					ItemPosition : "20"
				}]
			})
			.expectChange("salesOrderId", null)
			.expectChange("salesOrderId", "1")
			.expectChange("itemPosition", ["10", "20"])
			.expectChange("productName", null) // expect a later change
			.expectChange("supplierAddress", null); // expect a later change

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')"
						+ "/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct",
					requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						+ "/ToProduct"
				}, {
					__metadata : {
						uri : "ProductSet('P1')"
					},
					ProductID : "P1",
					Name : "Product 1"
				}, {"sap-message" : getMessageHeader(oMsgProductName)})
				.expectChange("productName", "Product 1")
				.expectMessage(oMsgProductName, "/ProductSet('P1')/",
					"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')"
					+ "/ToProduct/");

			// code under test
			that.oView.byId("detailProduct").setBindingContext(
				that.oView.byId("table").getItems()[0].getBindingContext()
			);

			return that.waitForChanges(assert);
		}).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')"
						+ "/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct"
						+ "/ToSupplier",
					requestUri : "ProductSet('P1')/ToSupplier"
				}, {
					__metadata : {
						uri : "BusinessPartnerSet('BP1')"
					},
					BusinessPartnerID : "BP1",
					Address : {
						City : "Walldorf"
					}
				}, {"sap-message" : getMessageHeader(oMsgSupplierAddress)})
				.expectChange("supplierAddress", "Walldorf")
				.expectMessage(oMsgSupplierAddress, "/BusinessPartnerSet('BP1')/",
					"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')"
					+ "/ToProduct/ToSupplier/");

			// code under test
			that.oView.byId("detailSupplier").setBindingContext(
				that.oView.byId("detailProduct").getBindingContext()
			);

			return that.waitForChanges(assert);
		}).then(function () {
			return Promise.all([
				that.checkValueState(assert, "productName", "Error", "Foo"),
				that.checkValueState(assert, "supplierAddress", "Warning", "Bar")
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: Messages are visualized at controls that are bound against the messages' target.
	// BCP: 2070436327: the data state is updated if unbindProperty is called
	QUnit.test("Messages: check value state", function (assert) {
		var oModel = createSalesOrdersModel(),
			oMsgGrossAmount = this.createResponseMessage("GrossAmount", "Foo", "warning"),
			oMsgNote = this.createResponseMessage("Note", "Bar"),
			that = this,
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="Note" value="{Note}" />\
	<Input id="GrossAmount" value="{GrossAmount}" />\
	<Input id="LifecycleStatusDescription" value="{LifecycleStatusDescription}" />\
</FlexBox>';

		this.expectRequest("SalesOrderSet('1')", {
				GrossAmount : "GrossAmount A",
				LifecycleStatusDescription : "LifecycleStatusDescription A",
				Note : "Note A"
			}, {"sap-message" : getMessageHeader([oMsgNote, oMsgGrossAmount])})
			.expectChange("Note", null)
			.expectChange("Note", "Note A")
			.expectChange("GrossAmount", null)
			.expectChange("GrossAmount", "GrossAmount A")
			.expectChange("LifecycleStatusDescription", null)
			.expectChange("LifecycleStatusDescription", "LifecycleStatusDescription A")
			.expectMessage(oMsgNote, "/SalesOrderSet('1')/")
			.expectMessage(oMsgGrossAmount, "/SalesOrderSet('1')/");

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			return Promise.all([
				that.checkValueState(assert, "Note", "Error", "Bar"),
				that.checkValueState(assert, "GrossAmount", "Warning", "Foo"),
				that.checkValueState(assert, "LifecycleStatusDescription", "None", ""),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			// code under test
			that.oView.byId("Note").unbindProperty("value");

			return Promise.all([
				that.checkValueState(assert, "Note", "None", ""),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			assert.strictEqual(that.oView.byId("Note").getValue(), "");
		});
	});

	//*********************************************************************************************
	// Scenario: Messages are not removed after refresh, if messages are flagged as persistent
	// (transient or transition)
	// JIRA: CPOUI5MODELS-35
[
	{
		bIsPersistent : true,
		sTarget : "Note",
		bTransient : true
	}, {
		bIsPersistent : true,
		sTarget : "Note",
		bTransition : true
	}, {
		bIsPersistent : true,
		sTarget : "/#TRANSIENT#Note"
	}, {
		bIsPersistent : false,
		sTarget : "Note"
	}
].forEach(function (oFixture) {
	var sTitle = "Messages: message is persistent=" + oFixture.bIsPersistent + " (transient="
			+ oFixture.bTransient + ", transition=" + oFixture.bTransition + ", target='"
			+ oFixture.sTarget + "')";

	QUnit.test(sTitle, function (assert) {
		var oExpectedMessage = {
				code : "code",
				fullTarget : "/SalesOrderSet('1')/Note",
				message : "Foo",
				persistent : oFixture.bIsPersistent,
				target : "/SalesOrderSet('1')/Note",
				type : MessageType.Error
			},
			oModel = createSalesOrdersModel(),
			that = this,
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="note" value="{Note}" />\
</FlexBox>';

		this.expectRequest("SalesOrderSet('1')", {
				SalesOrderID : "1",
				Note : "NoteA"
			}, {
				"sap-message" : getMessageHeader({
					code : "code",
					message : "Foo",
					severity : "error",
					target : oFixture.sTarget,
					transient : oFixture.bTransient,
					transition : oFixture.bTransition
				})
			})
			.expectChange("note", null)
			.expectChange("note", "NoteA")
			.expectMessages([oExpectedMessage]);

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			return that.checkValueState(assert, "note", "Error", "Foo");
		}).then(function () {
			that.expectRequest("SalesOrderSet('1')", {
					SalesOrderID : "1",
					Note : "NoteB"
				})
				.expectChange("note", "NoteB")
				.expectMessages(oFixture.bIsPersistent ? [oExpectedMessage] : []);

			// code under test
			that.oModel.refresh();

			return that.waitForChanges(assert);
		}).then(function () {
			return oFixture.bIsPersistent
				? that.checkValueState(assert, "note", "Error", "Foo")
				: that.checkValueState(assert, "note", "None", "");
		});
	});
});

	//*********************************************************************************************
	// Scenario: While refreshing a model or a binding, all messages belonging to that model or
	// binding have to be removed before new messages are reported.
	// BCP: 1970544211
	QUnit.test("Messages: refresh model or binding", function (assert) {
		var oModel = createSalesOrdersModelMessageScope(),
			oMsgProductAViaSalesOrder = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='3')/ToProduct('A')/Name"),
			oMsgProductAViaSalesOrderItem = cloneODataMessage(oMsgProductAViaSalesOrder,
				"(SalesOrderID='1',ItemPosition='3')/ToProduct('A')/Name"),
			oMsgSalesOrder = this.createResponseMessage(""),
			oMsgSalesOrderToLineItems1 = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='1')/ItemPosition"),
			oMsgSalesOrderToLineItems3 = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='3')/ItemPosition"),
			oMsgSalesOrderItem1 = cloneODataMessage(oMsgSalesOrderToLineItems1,
				"(SalesOrderID='1',ItemPosition='1')/ItemPosition"),
			oMsgSalesOrderItem3 = cloneODataMessage(oMsgSalesOrderToLineItems3,
				"(SalesOrderID='1',ItemPosition='3')/ItemPosition"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="id" text="{SalesOrderID}" />\
	<Table id="table" items="{ToLineItems}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				requestUri : "SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"}
			}, {
				SalesOrderID : "1"
			}, {"sap-message" : getMessageHeader([oMsgSalesOrder, oMsgSalesOrderToLineItems1])})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100",
				headers : {"sap-message-scope" : "BusinessObject"}
			}, {
				results : [
					{SalesOrderID : "1", ItemPosition : "1"},
					{SalesOrderID : "1", ItemPosition : "2"}
				]
			}, {"sap-message" : getMessageHeader(oMsgSalesOrderItem1)})
			.expectChange("id", null)
			.expectChange("id", "1")
			.expectChange("itemPosition", ["1", "2"])
			.expectMessage(oMsgSalesOrder, "/SalesOrderSet('1')")
			.expectMessage(oMsgSalesOrderItem1, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')",
					requestUri : "SalesOrderSet('1')",
					headers : {"sap-message-scope" : "BusinessObject"}
				}, {
					SalesOrderID : "1"
				}, {
					"sap-message" : getMessageHeader([
						oMsgSalesOrder,
						oMsgSalesOrderToLineItems3,
						// not in business object scope but service does not allow deeper navigation
						// path within the business object
						oMsgProductAViaSalesOrder
					])
				})
				.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100",
					headers : {"sap-message-scope" : "BusinessObject"}
				}, {
					results : [
						{SalesOrderID : "1", ItemPosition : "2"},
						{SalesOrderID : "1", ItemPosition : "3"}
					]
				}, {
					"sap-message" : getMessageHeader([
						oMsgSalesOrderItem3,
						// not in business object scope but service does not allow deeper navigation
						// path within the business object
						oMsgProductAViaSalesOrderItem
					])
				})
				.expectChange("itemPosition", ["2", "3"])
				.expectMessages([]) // clean all expected messages
				.expectMessage(oMsgSalesOrder, "/SalesOrderSet('1')")
				.expectMessage(oMsgSalesOrderItem3, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oMsgProductAViaSalesOrderItem,
					{isComplete : true, path : "/ProductSet('A')/Name"},
					"/SalesOrderSet('1')/ToLineItems");

			// code under test
			that.oModel.refresh();

			return that.waitForChanges(assert);
		}).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100",
					headers : {"sap-message-scope" : "BusinessObject"}
				}, {
					results : [
						{SalesOrderID : "1", ItemPosition : "3"},
						{SalesOrderID : "1", ItemPosition : "4"}
					]
				})
				.expectChange("itemPosition", ["3", "4"])
				.expectMessages([]) // clean all expected messages
				.expectMessage(oMsgSalesOrder, "/SalesOrderSet('1')");

			// code under test
			that.oView.byId("table").getBinding("items").refresh();

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: While paging in a table messages for non-affected rows must not be removed.
	// BCP: 1970544211
	QUnit.test("Messages: paging", function (assert) {
		var oModel = createSalesOrdersModelMessageScope(),
			oMsgProductA
				= this.createResponseMessage("(SalesOrderID='1',ItemPosition='1')/ToProduct/Name"),
			oMsgProductB
				= this.createResponseMessage("(SalesOrderID='1',ItemPosition='3')/ToProduct/Name"),
			oMsgSalesOrderItem1
				= this.createResponseMessage("(SalesOrderID='1',ItemPosition='1')/ItemPosition"),
			oMsgSalesOrderItem3
				= this.createResponseMessage("(SalesOrderID='1',ItemPosition='3')/ItemPosition"),
			sView = '\
<Table growing="true" growingThreshold="2" id="table"\
		items="{/SalesOrderSet(\'1\')/ToLineItems}">\
	<Text id="itemPosition" text="{ItemPosition}" />\
</Table>',
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2",
				headers : {"sap-message-scope" : "BusinessObject"}
			}, {
				results : [
					{SalesOrderID : "1", ItemPosition : "1"},
					{SalesOrderID : "1", ItemPosition : "2"}
				]
			}, {
				"sap-message" : getMessageHeader([
					oMsgSalesOrderItem1,
					// not in business object scope but service does not allow deeper navigation
					// path within the business object
					oMsgProductA
				])
			})
			.expectChange("itemPosition", ["1", "2"])
			.expectMessage(oMsgSalesOrderItem1, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems")
			.expectMessage(oMsgProductA, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=2&$top=2",
					headers : {"sap-message-scope" : "BusinessObject"}
				}, {
					results : [
						{SalesOrderID : "1", ItemPosition : "3"},
						{SalesOrderID : "1", ItemPosition : "4"}
					]
				}, {
					"sap-message" : getMessageHeader([
						oMsgSalesOrderItem3,
						// not in business object scope but service does not allow deeper navigation
						// path within the business object
						oMsgProductB
					])
				})
				.expectChange("itemPosition", ["3", "4"])
				.expectChange("itemPosition", ["3", "4"]) // TODO: why twice?
				.expectMessage(oMsgSalesOrderItem3, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oMsgProductB, "/SalesOrderLineItemSet",
						"/SalesOrderSet('1')/ToLineItems");

			// do paging
			that.oView.byId("table-trigger").firePress();

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Refresh of navigation properties previously pointing to the *same* entity responds
	// with a different entity for one of the navigation properties. Both navigation properties show
	// correct data on the UI. The fixed bugs are:
	// (1) the unchanged navigation property is updated with data from the changed one
	// (2) the changed navigation property is not updated
	// BCP: 1980535595
	QUnit.test("BCP 1980535595: refresh navigation properties to same entity", function (assert) {
		var sAdminDataRequest =
				"C_WorkCenterGroupAdminvData(ObjectTypeCode='G',ObjectInternalID='10000425')",
			oModel = createPPWorkcenterGroupModel(),
			sWCGroupRequest = "C_WorkCenterGroupTree(HierarchyRootNode='10000425'"
				+ ",HierarchyParentNode='00000000',HierarchyNode='10000425',HierarchyNodeType='G')",
			sView = '\
<FlexBox id="objectPage" binding="{\
path : \'/C_WorkCenterGroupTree(HierarchyRootNode=\\\'10000425\\\',\
HierarchyParentNode=\\\'00000000\\\',HierarchyNode=\\\'10000425\\\',HierarchyNodeType=\\\'G\\\')\',\
parameters : {createPreliminaryContext : true, canonicalRequest : true, \
usePreliminaryContext : false}}">\
	<FlexBox binding="{path : \'to_AdminData\',\
			parameters : {usePreliminaryContext : true, createPreliminaryContext : false}}">\
		<Text id="id" text="{ObjectInternalID}" />\
		<FlexBox binding="{path :\'to_CreatedByUserContactCard\',\
				parameters : {select : \'FullName\', createPreliminaryContext : false,\
					usePreliminaryContext : false}\
			}">\
			<Text id="createdName" text="{FullName}" />\
		</FlexBox>\
	</FlexBox>\
	<FlexBox binding="{path : \'to_AdminData\',\
			parameters : {usePreliminaryContext : true, createPreliminaryContext : false}}">\
		<FlexBox binding="{path :\'to_LastChangedByUserContactCard\',\
				parameters : {select : \'FullName\', createPreliminaryContext : false,\
					usePreliminaryContext : false}\
			}">\
			<Text id="lastChangedName" text="{FullName}" />\
		</FlexBox>\
	</FlexBox>\
</FlexBox>',
			that = this;

		this.expectRequest(sWCGroupRequest, {
				__metadata : {uri : "/" + sWCGroupRequest},
				HierarchyRootNode : "10000425",
				HierarchyParentNode : "00000000",
				HierarchyNode : "10000425",
				HierarchyNodeType : "G"
			})
			.expectRequest(sWCGroupRequest + "/to_AdminData", {
				__metadata : {uri : "/" + sAdminDataRequest},
				ObjectTypeCode : "G",
				ObjectInternalID : "10000425"
			})
			.expectRequest({
				deepPath : "/C_WorkCenterGroupTree(HierarchyRootNode='10000425',"
					+ "HierarchyParentNode='00000000',HierarchyNode='10000425',"
					+ "HierarchyNodeType='G')/to_AdminData/to_CreatedByUserContactCard",
				requestUri : sAdminDataRequest + "/to_CreatedByUserContactCard?$select=FullName"
			}, {
				__metadata : {uri : "/I_UserContactCard('Smith')"},
				FullName : "Smith"
			})
			.expectRequest({
				deepPath : "/C_WorkCenterGroupTree(HierarchyRootNode='10000425',"
					+ "HierarchyParentNode='00000000',HierarchyNode='10000425',"
					+ "HierarchyNodeType='G')/to_AdminData/to_LastChangedByUserContactCard",
				requestUri : sAdminDataRequest + "/to_LastChangedByUserContactCard?$select=FullName"
			}, {
				__metadata : {uri : "/I_UserContactCard('Smith')"},
				FullName : "Smith"
			})
			.expectChange("id", null)
			.expectChange("id", "10000425")
			.expectChange("createdName", null)
			.expectChange("createdName", "Smith")
			.expectChange("lastChangedName", null)
			.expectChange("lastChangedName", "Smith");

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest(sWCGroupRequest, {
					__metadata : {uri : "/" + sWCGroupRequest},
					HierarchyRootNode : "10000425",
					HierarchyParentNode : "00000000",
					HierarchyNode : "10000425",
					HierarchyNodeType : "G"
				})
				.expectRequest(sWCGroupRequest + "/to_AdminData", {
					__metadata : {uri : "/" + sAdminDataRequest},
					ObjectTypeCode : "G",
					ObjectInternalID : "10000425"
				})
				.expectRequest({
					deepPath : "/C_WorkCenterGroupTree(HierarchyRootNode='10000425',"
						+ "HierarchyParentNode='00000000',HierarchyNode='10000425',"
						+ "HierarchyNodeType='G')/to_AdminData/to_CreatedByUserContactCard",
					requestUri : sAdminDataRequest + "/to_CreatedByUserContactCard?$select=FullName"
				}, {
					__metadata : {uri : "/I_UserContactCard('Smith')"},
					FullName : "Smith"
				})
				.expectRequest({
					deepPath : "/C_WorkCenterGroupTree(HierarchyRootNode='10000425',"
						+ "HierarchyParentNode='00000000',HierarchyNode='10000425',"
						+ "HierarchyNodeType='G')/to_AdminData/to_LastChangedByUserContactCard",
					requestUri : sAdminDataRequest
						+ "/to_LastChangedByUserContactCard?$select=FullName"
				}, {
					__metadata : {uri : "/I_UserContactCard('Muller')"},
					FullName : "Muller"
				})
				.expectChange("id", null)
				.expectChange("id", "10000425")
				.expectChange("createdName", null)
				.expectChange("createdName", "Smith")
				.expectChange("lastChangedName", null)
				.expectChange("lastChangedName", "Muller");

			// code under test: refresh keeps canonical path for "/I_UserContactCard('Smith')" in
			// ODataModel#mPathCache and does *not* replace it by "/I_UserContactCard('Muller')";
			// all other path cache entries for "to_LastChangedByUserContactCard" are adapted
			that.oView.byId("objectPage").getObjectBinding().refresh(/*bForceUpdate*/true);

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Use reduced paths for the messages' full target path.
	// A modification of an item causes sideeffects on the header, so the item and the header data
	// need to be updated via a GET request on the item, using $expand for the header data.
	// The backend returns messages with a target relative to the item. So the targets for header
	// messages will contain partner navigation properties that have to be removed.
	// JIRA: CPOUI5MODELS-82
	QUnit.test("Use reduced paths for the messages' full target path", function (assert) {
		var oModel = createSalesOrdersModel({preliminaryContext : true}),
			oSalesOrderGrossAmountError = this.createResponseMessage("GrossAmount"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="grossAmount" value="{GrossAmount}" />\
	<Table id="table" items="{ToLineItems}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="grossAmount::item" value="{GrossAmount}" />\
		<Input id="currencyCode" value="{CurrencyCode}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", {
				__metadata : {uri : "SalesOrderSet('1')"},
				GrossAmount : "0.00",
				SalesOrderID : "1"
			}, {"sap-message" : getMessageHeader(oSalesOrderGrossAmountError)})
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectChange("grossAmount", null)
			.expectChange("grossAmount", "0.00")
			.expectRequest("SalesOrderSet('1')/ToLineItems?$skip=0&$top=100", {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					CurrencyCode : "EUR",
					GrossAmount : "0.00",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~"])
			.expectChange("grossAmount::item", ["0.00"])
			.expectChange("currencyCode", ["EUR"])
			.expectMessage(oSalesOrderGrossAmountError, "/SalesOrderSet('1')/");

		return this.createView(assert, sView, oModel).then(function () {
			var oContext = that.oView.byId("table").getItems()[0].getBindingContext(),
				oSalesOrderItemToHeaderGrossAmountError
					= that.createResponseMessage("ToHeader/GrossAmount");

			that.expectRequest({
					deepPath : "/SalesOrderSet('1')"
						+ "/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')",
					requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						+ "?$expand=ToHeader"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					SalesOrderID : "1",
					ItemPosition : "10~0~",
					GrossAmount : "1000.00",
					ToHeader : {
						__metadata : {uri : "SalesOrderSet('1')"},
						SalesOrderID : "1",
						GrossAmount : "1000.00"
					}
				}, {"sap-message" : getMessageHeader(oSalesOrderItemToHeaderGrossAmountError)})
				.expectChange("grossAmount", "1000.00")
				.expectChange("grossAmount::item", ["1000.00"])
				.expectMessages([{
					code : oSalesOrderItemToHeaderGrossAmountError.code,
					fullTarget : "/SalesOrderSet('1')/GrossAmount",
					message : oSalesOrderItemToHeaderGrossAmountError.message,
					persistent : false,
					target : "/SalesOrderSet('1')/GrossAmount",
					type : mSeverityMap[oSalesOrderItemToHeaderGrossAmountError.severity]
				}]);

			// code under test
			oModel.read("", {
				context : oContext,
				urlParameters : {$expand : "ToHeader"}
			});

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: The ODataModel#create API is called with an object (partially) retrieved via
	// ODataModel#getObject which may contain properties inside a __metadata property which are not
	// specified by OData so that a request would fail on the server. The request payload is
	// cleaned up before the request is sent.
	// BCP: 002075129400000695502020
	// BCP: 002075129500001965532020
	QUnit.test("create payload only contains cleaned up __metadata", function (assert) {
		var oModel = createSalesOrdersModel({tokenHandling : false}),
			sView = '\
<FlexBox binding="{path : \'/SalesOrderSet(\\\'1\\\')\',\
		parameters : {select : \'SalesOrderID,Note\', expand : \'ToLineItems\'}}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table id="table" items="{path : \'ToLineItems\',\
			parameters : {select : \'ItemPosition,Note,SalesOrderID\'}}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')?$select=SalesOrderID%2cNote&$expand=ToLineItems", {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Note",
				SalesOrderID : "1",
				ToLineItems : {
					results : [{
						__metadata : {
							uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						},
						ItemPosition : "10~0~",
						Note : "ItemNote",
						SalesOrderID : "1"
					}]
				}
			})
			.expectChange("note", null)
			.expectChange("note", "Note")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectChange("itemPosition", ["10~0~"])
			.expectChange("note::item", ["ItemNote"]);

		return this.createView(assert, sView, oModel).then(function () {
			// avoid MERGE on property change
			oModel.setChangeGroups(
				{SalesOrderLineItem : {groupId : "never"}},
				{"*" : {groupId : "change"}}
			);
			oModel.setDeferredGroups(["change", "never"]);

			that.expectChange("note::item", "ItemNote Changed", 0);

			// code under test: leads to __metadata.deepPath being set in the item data
			oModel.setProperty("/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')/Note",
				"ItemNote Changed");

			return that.waitForChanges(assert);
		}).then(function () {
			var oData;

			that.expectRequest({
				created : true,
				data : {
					SalesOrderID : "2",
					ToLineItems : [{
						Note : "ItemNote Changed",
						__metadata : {
							uri: "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
							// Note: Payload must not contain deepPath
						}
					}]
				},
				deepPath : "/SalesOrderSet",
				entityTypes : {
					"GWSAMPLE_BASIC.SalesOrder" : true
				},
				method : "POST",
				requestUri : "SalesOrderSet"
			});

			// code under test
			oData = oModel.getObject("/SalesOrderSet('1')", null,
				{select : "SalesOrderID,ToLineItems/Note", expand : "ToLineItems"});
			oModel.create("/SalesOrderSet", {SalesOrderID : "2", ToLineItems : oData.ToLineItems});
			oModel.submitChanges({groupId : "change"});

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Keep user input and validation error after updating an entity if entity data is
	// invalidated via <code>sap.ui.model.odata.v2.ODataModel#invalidateEntry</code> by the
	// application.
	// BCP: 2080018339
	QUnit.test("Keep user input and validation error after updating an entity", function (assert) {
		var oModel = createSalesOrdersModel({
				defaultBindingMode : BindingMode.TwoWay,
				preliminaryContext : true,
				refreshAfterChange : true
			}),
			oNoteInput,
			sView = '\
<FlexBox id="form" binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{\
			path : \'Note\',\
			type : \'sap.ui.model.odata.type.String\',\
			constraints : {maxLength : 3}\
		}" />\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Bar",
				SalesOrderID : "1"
			})
			// Do not expect changes for "note" because setting the value of the "note" input field
			// via control is not possible if "expectChange" is used.
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1");

		return this.createView(assert, sView, oModel).then(function () {
			oNoteInput = that.oView.byId("note");

			assert.strictEqual(oNoteInput.getValue(), "Bar");

			that.expectMessages([{
					code : undefined,
					descriptionUrl : undefined,
					fullTarget : "",
					message : "Enter a text with a maximum of 3 characters and spaces",
					persistent : false,
					target : oNoteInput.getId() + "/value",
					technical : false,
					type : "Error"
				}]);

			// code under test - produce a validation error
			oNoteInput.setValue("abcd");

			return that.waitForChanges(assert);
		}).then(function () {
			var oElementBinding = that.oView.byId("form").getElementBinding();

			that.expectRequest("SalesOrderSet('1')", {
					__metadata : {uri : "SalesOrderSet('1')"},
					Note : "Bar",
					SalesOrderID : "1"
				});

			// code under test
			oModel.invalidateEntry(oElementBinding.getBoundContext());
			oElementBinding.refresh(true);

			return that.waitForChanges(assert);
		}).then(function () {
			// check validation error and current value on the UI
			that.checkValueState(assert, oNoteInput, "Error",
				"Enter a text with a maximum of 3 characters and spaces");
			assert.strictEqual(oNoteInput.getValue(), "abcd");

			return that.waitForChanges(assert);
		}).then(function () {
			that.expectRequest("SalesOrderSet('2')", {
					__metadata : {uri : "SalesOrderSet('2')"},
					// model value is not changed -> no change event is triggered for that property
					Note : "Bar",
					SalesOrderID : "2"
				})
				.expectChange("salesOrderID", "2")
				.expectMessages([]);

			// code under test - rebinding the form causes cleanup of the validation error and the
			// user input
			that.oView.byId("form").bindObject("/SalesOrderSet('2')");

			return that.waitForChanges(assert);
		}).then(function () {
			// code under test - check validation error and current value on the UI reverted
			that.checkValueState(assert, oNoteInput, "None", "");
			assert.strictEqual(oNoteInput.getValue(), "Bar");

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. All the
	// sales order items are returned with the initial request. Check the lifecycle of stateful
	// OData messages.
	// Expectation: All messages for the sales order and all messages for the sales order items are
	// displayed. In case of message scope BusinessObject also messages for child entities are
	// displayed.
	// JIRA: CPOUI5MODELS-111
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (1), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItemNoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderToItemPositionError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ItemPosition"),
			oSalesOrderItemNoteError = cloneODataMessage(oSalesOrderToItemNoteError,
				"(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderItemPositionError = cloneODataMessage(oSalesOrderToItemPositionError,
				"(SalesOrderID='1',ItemPosition='10~0~')/ItemPosition"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItemNoteError, oSalesOrderToItemPositionError]
					: [oSalesOrderNoteError])
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~"])
			.expectChange("note::item", ["Bar"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderItemNoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItemPositionError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/");
		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel);
	});
});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. Only a
	// part of the sales order items is displayed after the initial load. After initial load trigger
	// paging to get more sales order items. Check the lifecycle of stateful OData messages.
	// Expectation: In case of message scope BusinessObject all messages for the sales order and all
	// messages for the sales order items are displayed. Also all messages for child entities are
	// displayed. In case of message scope RequestedObjects only the messages for the sales order
	// and the messages for the requested sales order items are displayed.
	// Paging must not remove messages of items that are already on the client. New messages for the
	// sales order items are added, if they are not yet contained in the message model.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (2) + (3), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItem10NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderToItem30NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='30~2~')/Note"),
			oSalesOrderItem10NoteError = cloneODataMessage(oSalesOrderToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderItem30NoteError = cloneODataMessage(oSalesOrderToItem30NoteError,
				"(SalesOrderID='1',ItemPosition='30~2~')/Note"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table growing="true" growingThreshold="2" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItem10NoteError, oSalesOrderToItem30NoteError]
					: oSalesOrderNoteError)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					Note : "Baz",
					ItemPosition : "20~1~",
					SalesOrderID : "1"
				}]
			}/*, { // message is not sent because of transitionMessagesOnly
				"sap-message" : getMessageHeader(oSalesOrderItem10NoteError)
			}*/)
			.expectChange("itemPosition", ["10~0~", "20~1~"])
			.expectChange("note::item", ["Bar", "Baz"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
				.expectMessage(oSalesOrderItem10NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem30NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");
		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=2&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~2~')"
					},
					Note : "Qux",
					ItemPosition : "30~2~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='40~3~')"
					},
					Note : "Quux",
					ItemPosition : "40~3~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", "30~2~", 2)
			.expectChange("itemPosition", "40~3~", 3)
			.expectChange("note::item", "Qux", 2)
			.expectChange("note::item", "Quux", 3);

			// do paging
			that.oView.byId("table-trigger").firePress();

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. Only a
	// part of the sales order items is displayed after the initial load. After initial load
	// user filters the list, that some entries are filtered out and some additional are displayed.
	// Check the lifecycle of stateful OData messages.
	// Expectation: In case of message scope "BusinessObject", the filter request must not delete
	// messages for entities that have been filtered out because the list is embedded into an
	// object page. The topmost entity is responsible for the message handling.
	// In case of message scope "Request", old messages are untouched, new messages are added.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (4), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItem10GrossAmountError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/GrossAmount"),
			oSalesOrderToItem20GrossAmountError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='20~1~')/GrossAmount"),
			oSalesOrderToItem30GrossAmountError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='30~1~')/GrossAmount"),
			oSalesOrderItem10GrossAmountError
				= cloneODataMessage(oSalesOrderToItem10GrossAmountError,
					"(SalesOrderID='1',ItemPosition='10~0~')/GrossAmount"),
			oSalesOrderItem20GrossAmountError
				= cloneODataMessage(oSalesOrderToItem20GrossAmountError,
					"(SalesOrderID='1',ItemPosition='20~1~')/GrossAmount"),
			oSalesOrderItem30GrossAmountError
				= cloneODataMessage(oSalesOrderToItem30GrossAmountError,
					"(SalesOrderID='1',ItemPosition='30~1~')/GrossAmount"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table growing="true" growingThreshold="2" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="grossAmount" value="{GrossAmount}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItem10GrossAmountError, oSalesOrderToItem20GrossAmountError,
						oSalesOrderToItem30GrossAmountError]
					: oSalesOrderNoteError)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					GrossAmount : "111.0",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					GrossAmount : "42.0",
					ItemPosition : "20~1~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~", "20~1~"])
			.expectChange("grossAmount", ["111.0", "42.0"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
				.expectMessage(oSalesOrderItem10GrossAmountError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem20GrossAmountError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem30GrossAmountError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");

		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function() {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					headers : bWithMessageScope
						? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
						: {"sap-messages" : "transientOnly"},
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
						+ "&$filter=GrossAmount gt 100.0m"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						},
						GrossAmount : "111.0",
						ItemPosition : "10~0~",
						SalesOrderID : "1"
					}, {
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~1~')"
						},
						GrossAmount : "222.0",
						ItemPosition : "30~1~",
						SalesOrderID : "1"
					}]
				})
				.expectChange("itemPosition", "30~1~", 1)
				.expectChange("grossAmount", "222.0", 1);

			// Code under test
			that.oView.byId("table").getBinding("items").filter([new Filter({
				path : 'GrossAmount',
				operator : FilterOperator.GT,
				value1 : "100.0"
			})]);

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. There are
	// two lists for the sales order items with different filter values. After data is loaded user
	// refreshs one list and gets different data / messages. Check the lifecycle of stateful OData
	// messages.
	// Expectation: In case of message scope "BusinessObjects" all messages for the sales order
	// add all messages for the child entities of the sales order are displayed. Refreshing a list
	// must not remove messages for the other list. Refreshing the messages has to be triggered via
	// root entity of the object page.
	// In case of message scope "Request" only the messages for the sales order and all messages for
	// both item list are displayed.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (5) + (6), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItem10GrossAmountError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/GrossAmount"),
			oSalesOrderToItem20GrossAmountError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='20~0~')/GrossAmount"),
			oSalesOrderToItem30GrossAmountError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='30~0~')/GrossAmount"),
			oSalesOrderItem10GrossAmountError
				= cloneODataMessage(oSalesOrderToItem10GrossAmountError,
					"(SalesOrderID='1',ItemPosition='10~0~')/GrossAmount"),
			oSalesOrderItem20GrossAmountError
				= cloneODataMessage(oSalesOrderToItem20GrossAmountError,
					"(SalesOrderID='1',ItemPosition='20~0~')/GrossAmount"),
			oSalesOrderItem30GrossAmountError
				= cloneODataMessage(oSalesOrderToItem30GrossAmountError,
					"(SalesOrderID='1',ItemPosition='30~0~')/GrossAmount"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table id="table1" items="{path : \'ToLineItems\', \
		parameters : {transitionMessagesOnly : true},\
		filters : {path : \'GrossAmount\', operator : \'GT\', value1 : \'100.0\'}\
	}">\
		<Text id="itemPosition1" text="{ItemPosition}" />\
		<Input id="grossAmount1" value="{GrossAmount}" />\
	</Table>\
	<Table id="table2" items="{path : \'ToLineItems\', \
		parameters : {transitionMessagesOnly : true},\
		filters : {path : \'GrossAmount\', operator : \'LE\', value1 : \'100.0\'}\
	}">\
		<Text id="itemPosition2" text="{ItemPosition}" />\
		<Input id="grossAmount2" value="{GrossAmount}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItem10GrossAmountError, oSalesOrderToItem20GrossAmountError,
						oSalesOrderToItem30GrossAmountError]
					: oSalesOrderNoteError)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100&"
					+ "$filter=GrossAmount gt 100.0m"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					GrossAmount : "111.0",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition1", ["10~0~"])
			.expectChange("grossAmount1", ["111.0"])
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100&"
					+ "$filter=GrossAmount le 100.0m"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~0~')"
					},
					GrossAmount : "42.0",
					ItemPosition : "20~0~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition2", ["20~0~"])
			.expectChange("grossAmount2", ["42.0"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
				.expectMessage(oSalesOrderItem10GrossAmountError, {
					isComplete : true,
					path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						+ "/GrossAmount"
				}, "/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem20GrossAmountError, {
					isComplete : true,
					path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~0~')"
						+ "/GrossAmount"
				}, "/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem30GrossAmountError, {
					isComplete : true,
					path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~0~')"
						+ "/GrossAmount"
				}, "/SalesOrderSet('1')/ToLineItems");
		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			// assume a sideeffect removed entity (SalesOrderID='1',ItemPosition='10~0~')
			// with refresh only the data and the messages for that data is updated
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					headers : bWithMessageScope
						? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
						: {"sap-messages" : "transientOnly"},
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100&"
						+ "$filter=GrossAmount gt 100.0m"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~0~')"
						},
						GrossAmount : "123.0",
						ItemPosition : "30~0~",
						SalesOrderID : "1"
					}]
				})
				.expectChange("itemPosition1", ["30~0~"])
				.expectChange("grossAmount1", ["123.0"]);

			// Code under test
			that.oView.byId("table1").getBinding("items").refresh();

			return that.waitForChanges(assert);
		}).then(function () {
			// to update also the messages, the messages need to be read again via the root entity
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')",
					headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
					requestUri : "SalesOrderSet('1')?$select=SalesOrderID"
				}, {
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}, {
					"sap-message" : getMessageHeader(bWithMessageScope
						? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
							oSalesOrderToItem20GrossAmountError,
							oSalesOrderToItem30GrossAmountError]
						: oSalesOrderNoteError)
				})
				.expectMessages([]) // clean all expected messages
				.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

			if (bWithMessageScope) {
				that.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
					.expectMessage(oSalesOrderItem20GrossAmountError, {
						isComplete : true,
						path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~0~')"
							+ "/GrossAmount"
					}, "/SalesOrderSet('1')/ToLineItems")
					.expectMessage(oSalesOrderToItem30GrossAmountError, {
							isComplete : true,
							path : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~0~')"
								+ "/GrossAmount"
						}, "/SalesOrderSet('1')/");
			}

			// code under test
			oModel.read("/SalesOrderSet('1')", {
				updateAggregatedMessages : true,
				urlParameters : {
					$select : "SalesOrderID"
				}
			});

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. Only a
	// part of the sales order items is displayed after the initial load. After initial load
	// sort the list and get other sales order items. Check the lifecycle of stateful OData
	// messages.
	// Expectation: In case of message scope BusinessObject all messages for the sales order and all
	// messages for the sales order items are displayed. Also all messages for child entities are
	// displayed. In case of message scope RequestedObjects only the messages for the sales order
	// and the messages for the requested sales order items are displayed.
	// Sorting must not remove messages of items, new messages for the sales order items are added,
	// if they are not yet contained in the message model.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (2) + (7), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItem10NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderToItem30NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='30~0~')/Note"),
			oSalesOrderItem10NoteError = cloneODataMessage(oSalesOrderToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderItem30NoteError = cloneODataMessage(oSalesOrderToItem30NoteError,
				"(SalesOrderID='1',ItemPosition='30~0~')/Note"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table growing="true" growingThreshold="2" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItem10NoteError, oSalesOrderToItem30NoteError]
					: oSalesOrderNoteError)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					Note : "Baz",
					ItemPosition : "20~1~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~", "20~1~"])
			.expectChange("note::item", ["Bar", "Baz"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
				.expectMessage(oSalesOrderItem10NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem30NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");

		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					headers : bWithMessageScope
						? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
						: {"sap-messages" : "transientOnly"},
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
						+ "&$orderby=GrossAmount asc"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~0~')"
						},
						Note : "Qux",
						ItemPosition : "30~0~",
						SalesOrderID : "1"
					}, {
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='40~1~')"
						},
						Note : "Quux",
						ItemPosition : "40~1~",
						SalesOrderID : "1"
					}]
				})
				.expectChange("itemPosition", ["30~0~", "40~1~"])
				.expectChange("note::item", ["Qux", "Quux"]);

			// Code under test
			that.oView.byId("table").getBinding("items").sort(new Sorter("GrossAmount"));

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. Only a
	// part of the sales order items is displayed after the initial load. After initial load
	// modify first a sales order item and later the sales order itself to get rid of messages.
	// Check the lifecycle of stateful OData messages.
	// Expectation: In case of message scope BusinessObject all messages for the sales order and all
	// messages for the sales order items are displayed. Also all messages for child entities are
	// displayed. After modifying the entities the corresponding messages are removed.
	// In case of message scope RequestedObjects only the messages for the sales order and the
	// messages for the requested sales order items are displayed. After modifying the entities the
	// corresponding messages are removed.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
	// BCP: 1980510782
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (8), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				canonicalRequests : true,
				preliminaryContext : true,
				refreshAfterChange : false
			}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItem10ToProductPriceError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderToItem10NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderToItem30NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='30~0~')/Note"),
			oSalesOrderItem10NoteError = cloneODataMessage(oSalesOrderToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderItem30NoteError = cloneODataMessage(oSalesOrderToItem30NoteError,
				"(SalesOrderID='1',ItemPosition='30~0~')/Note"),
			oSalesOrderItem10ToProductPriceError
				= cloneODataMessage(oSalesOrderToItem10ToProductPriceError,
					"(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table growing="true" growingThreshold="2" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItem10NoteError, oSalesOrderToItem10ToProductPriceError,
						oSalesOrderToItem30NoteError]
					: oSalesOrderNoteError)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					Note : "Baz",
					ItemPosition : "20~1~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~", "20~1~"])
			.expectChange("note::item", ["Bar", "Baz"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
				.expectMessage(oSalesOrderItem10NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem30NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem10ToProductPriceError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");
		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			var oItem10ToProductPriceError = cloneODataMessage(oSalesOrderItem10ToProductPriceError,
					"ToProduct/Price");

			that.expectChange("note::item", "Qux", 0)
				.expectHeadRequest(bWithMessageScope
					? {"sap-message-scope" : "BusinessObject"}
					: {})
				.expectRequest({
					data : {
						Note : "Qux",
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						}
					},
					deepPath :
						"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')",
					headers : bWithMessageScope
						? {"sap-message-scope" : "BusinessObject", "x-http-method" : "MERGE"}
						: {"x-http-method" : "MERGE"},
					key : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')",
					method : "POST",
					requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
				}, NO_CONTENT, bWithMessageScope
					? {"sap-message" : getMessageHeader(oItem10ToProductPriceError)}
					: undefined
				)
				.expectMessages([]) // clean all expected messages
				.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

			if (bWithMessageScope) {
				that.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
					.expectMessage(oItem10ToProductPriceError,
						"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')/",
						"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/")
					.expectMessage(oSalesOrderItem30NoteError,
						"/SalesOrderLineItemSet", "/SalesOrderSet('1')/ToLineItems");
			}

			// code under test - modify a sales order item
			oModel.setProperty(
				"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note",
				"Qux");
			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			that.expectChange("note", "Quxx")
				.expectRequest({
					data : {
						Note : "Quxx",
						__metadata : {uri : "SalesOrderSet('1')"}
					},
					deepPath :
						"/SalesOrderSet('1')",
					headers : bWithMessageScope
						? {"sap-message-scope" : "BusinessObject", "x-http-method" : "MERGE"}
						: {"x-http-method" : "MERGE"},
					key : "SalesOrderSet('1')",
					method : "POST",
					requestUri : "SalesOrderSet('1')"
				}, NO_CONTENT, bWithMessageScope
					? {"sap-message" : getMessageHeader(oSalesOrderToItem30NoteError)}
					: undefined
				)
				.expectMessages([]) // clean all expected messages
				.expectMessage(bWithMessageScope ? oSalesOrderItem30NoteError : null,
					"/SalesOrderLineItemSet", "/SalesOrderSet('1')/ToLineItems");

			// code under test - modify the sales order
			oModel.setProperty("/SalesOrderSet('1')/Note", "Quxx");
			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. Only a
	// part of the sales order items is displayed after the initial load. After initial load
	// delete one entry from the list. Check the lifecycle of stateful OData messages.
	// Expectation: In case of message scope BusinessObject all messages for the sales order and all
	// messages for the sales order items are displayed. Also all messages for child entities are
	// displayed. After the deletion of an entity all messages for that entity and all its child
	// entities are removed.
	// In case of message scope RequestedObjects only the messages for the sales order and the
	// messages for the requested sales order items are displayed. After the deletion only the
	// messages for the deleted entity are removed.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	QUnit.test("Message lifecycle (10), scope: " + sMessageScope, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToBusinessPartnerAddress
				= this.createResponseMessage("ToBusinessPartner/Address"),
			oSalesOrderToItem10ToProductPriceError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderToItem10NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderToItem20NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='20~1~')/Note"),
			oSalesOrderItem10NoteError = cloneODataMessage(oSalesOrderToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderItem10ToProductPriceError
				= cloneODataMessage(oSalesOrderToItem10ToProductPriceError,
					"(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderItem20NoteError = cloneODataMessage(oSalesOrderToItem20NoteError,
				"(SalesOrderID='1',ItemPosition='20~1~')/Note"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table growing="true" growingThreshold="2" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrderNoteError, oSalesOrderToBusinessPartnerAddress,
						oSalesOrderToItem10NoteError, oSalesOrderToItem10ToProductPriceError,
						oSalesOrderToItem20NoteError]
					: oSalesOrderNoteError)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : bWithMessageScope
					? {"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"}
					: {"sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					Note : "Baz",
					ItemPosition : "20~1~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~", "20~1~"])
			.expectChange("note::item", ["Bar", "Baz"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

		if (bWithMessageScope) {
			this.expectMessage(oSalesOrderToBusinessPartnerAddress, "/SalesOrderSet('1')/")
				.expectMessage(oSalesOrderItem10NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem10ToProductPriceError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oSalesOrderItem20NoteError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");
		}
		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectHeadRequest(bWithMessageScope
					? {"sap-message-scope" : "BusinessObject"}
					: {})
				.expectRequest({
					deepPath :
						"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10~0~')",
					headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
					method : "DELETE",
					requestUri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
				}, NO_CONTENT)
				// ODataModel#remove does not remove the item from the list. A refresh needs to be
				// triggered or refreshAfterChange has to be true to trigger refresh automatically
				.expectChange("itemPosition", undefined, 0)
				.expectChange("note::item", undefined, 0)
				.expectMessages([]) // clean all expected messages
				.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/")
				.expectMessage(bWithMessageScope ? oSalesOrderToBusinessPartnerAddress : null,
					"/SalesOrderSet('1')/")
				.expectMessage(bWithMessageScope ? oSalesOrderItem20NoteError : null,
					"/SalesOrderLineItemSet", "/SalesOrderSet('1')/ToLineItems");

			// code under test
			oModel.remove("", {
				context : that.oView.byId("table").getItems()[0].getBindingContext(),
				refreshAfterChange : false
			});
			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: On a list report page a list of sales orders is displayed. Only a part of the sales
	// orders is displayed after the initial load. After the data is displayed trigger a refresh or
	// filter the list. Check the lifecycle of stateful OData messages.
	// Expectation: In case of message scope BusinessObject all messages for the sales orders and
	// all messages for their child entities are displayed. After the refresh/filtering all messages
	// for all sales orders and all their child entities are removed and replaced by the messages
	// contained in the response of the refresh/filter request.
	// In case of message scope RequestedObjects only the messages for the requested sales orders
	// are displayed. A refresh/filtering does not cause a deletion of messages for entities that
	// are not returned any more.
	// JIRA: CPOUI5MODELS-111, CPOUI5MODELS-112
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	[true, false].forEach(function (bFilter) {
	var sTitle = "Message lifecycle (11), scope: " + sMessageScope + ", bFilter: " + bFilter;

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrder1NoteError = this.createResponseMessage("('1~0~')/Note"),
			oSalesOrder1ToBusinessPartnerAddress
				= this.createResponseMessage("('1~0~')/ToBusinessPartner/Address"),
			oSalesOrder2NoteError = this.createResponseMessage("('2~1~')/Note"),
			oSalesOrder2ToBusinessPartnerAddress
				= this.createResponseMessage("('2~1~')/ToBusinessPartner/Address"),
			sView = '\
<Table growing="true" growingThreshold="2" id="table" items="{/SalesOrderSet}">\
	<Input id="note" value="{Note}" />\
</Table>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {uri : "SalesOrderSet('1~0~')"},
					Note : "Foo",
					SalesOrderID : "1~0~"
				}, {
					__metadata : {uri : "SalesOrderSet('2~1~')"},
					Note : "Bar",
					SalesOrderID : "2~1~"
				}]
			}, {
				"sap-message" : getMessageHeader(bWithMessageScope
					? [oSalesOrder1NoteError, oSalesOrder1ToBusinessPartnerAddress,
						oSalesOrder2NoteError, oSalesOrder2ToBusinessPartnerAddress]
					: [oSalesOrder1NoteError, oSalesOrder2NoteError])
			})
			.expectChange("note", ["Foo", "Bar"])
			.expectMessage(oSalesOrder1NoteError, "/SalesOrderSet")
			.expectMessage(bWithMessageScope ? oSalesOrder1ToBusinessPartnerAddress : null,
				"/SalesOrderSet")
			.expectMessage(oSalesOrder2NoteError, "/SalesOrderSet")
			.expectMessage(bWithMessageScope ? oSalesOrder2ToBusinessPartnerAddress : null,
				"/SalesOrderSet");

		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			var oSalesOrder3NoteError = that.createResponseMessage("('3~1~')/Note");

			that.expectRequest({
					deepPath : "/SalesOrderSet",
					headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
					requestUri : "SalesOrderSet?$skip=0&$top=2"
						+ (bFilter ? "&$filter=GrossAmount gt 100.0m" : "")
				}, {
					results : [{
						__metadata : {uri : "SalesOrderSet('1~0~')"},
						Note : "Foo",
						SalesOrderID : "1~0~"
					},
					// "SalesOrderSet('2~1~')" has been filtered out, or in case of a refresh the
					// entity has been removed in meantime
					{
						__metadata : {
							uri : "SalesOrderSet('3~1~')"
						},
						Note : "Baz",
						SalesOrderID : "3~1~"
					}]
				}, {
					"sap-message" : getMessageHeader(bWithMessageScope
						? [oSalesOrder1NoteError, oSalesOrder1ToBusinessPartnerAddress,
							oSalesOrder3NoteError]
						: [oSalesOrder1NoteError, oSalesOrder3NoteError])
				})
				.expectChange("note", "Baz", 1)
				.expectMessages([]) // clean all expected messages
				.expectMessage(oSalesOrder1NoteError, "/SalesOrderSet")
				.expectMessage(bWithMessageScope ? oSalesOrder1ToBusinessPartnerAddress : null,
					"/SalesOrderSet")
				//TODO: MessageScope.RequestedObjects: how to get rid of messages of entities that
				// are removed from the list?
				.expectMessage(!bWithMessageScope ? oSalesOrder2NoteError : null, "/SalesOrderSet")
				.expectMessage(oSalesOrder3NoteError, "/SalesOrderSet");

			if (bFilter) {
				// code under test
				that.oView.byId("table").getBinding("items").filter([new Filter({
					path : 'GrossAmount',
					operator : FilterOperator.GT,
					value1 : "100.0"
				})]);
			} else {
				// code under test - somehow the data changed - refresh to get current data
				that.oView.byId("table").getBinding("items").refresh();
			}

			return that.waitForChanges(assert);
		});
	});
	});
});

	//*********************************************************************************************
	// Scenario: Show entity with sub entities in same business object. Both have massages. Refresh
	// of entity should also lead to update of aggregated messages for the sub entities.
	// JIRA: CPOUI5MODELS-151
	QUnit.test("ODataModel#createBindingContext with updateAggregatedMessages", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({preliminaryContext : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToItem10NoteError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			oSalesOrderItem10NoteError = cloneODataMessage(oSalesOrderToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10~0~')/Note"),
			sView = '\
<FlexBox id="form" binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
</FlexBox>',
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader(
					[oSalesOrderNoteError, oSalesOrderToItem10NoteError])
			})
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/")
			.expectMessage(oSalesOrderItem10NoteError, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems");
		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')",
					headers : {"sap-message-scope" : "BusinessObject"},
					requestUri : "SalesOrderSet('1')"
				}, {
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}, {
					"sap-message" : getMessageHeader([oSalesOrderNoteError])
				})
				.expectMessages([])
				.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/");

			// code under test
			that.oView.byId("form").getObjectBinding().refresh();

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: On an object page a sales order with its sales order items is displayed. Only a
	// part of the sales order items are displayed. Some of the items have messages. Filter the
	// items table by entries having messages.
	// JIRA: CPOUI5MODELS-106
	QUnit.test("Filter table by items with messages", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				preliminaryContext : true,
				useBatch : true
			}),
			oItemsBinding,
			oSalesOrderDeliveryStatusAndToItemError = this.createResponseMessage(
				["DeliveryStatus", "ToLineItems(SalesOrderID='1',ItemPosition='40~2~')/Quantity"]),
			oSalesOrderDeliveryStatusAndItemError =
				cloneODataMessage(oSalesOrderDeliveryStatusAndToItemError,
					"DeliveryStatus", ["(SalesOrderID='1',ItemPosition='40~2~')/Quantity"]),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToItemsError = this.createResponseMessage("ToLineItems"),
			oSalesOrderToItem10ToProductPriceError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderToItem20NoteWarning = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='20~1~')/Note", undefined, "warning"),
			oSalesOrderToItem30NoteError = this.createResponseMessage(
				["ToLineItems(SalesOrderID='1',ItemPosition='30~1~')/Note",
				 "ToLineItems(SalesOrderID='1',ItemPosition='30~1~')/GrossAmount"]),
			oSalesOrderItem10ToProductPriceError
				= cloneODataMessage(oSalesOrderToItem10ToProductPriceError,
					"(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderItem20NoteWarning = cloneODataMessage(oSalesOrderToItem20NoteWarning,
				"(SalesOrderID='1',ItemPosition='20~1~')/Note"),
			oSalesOrderItem30NoteError = cloneODataMessage(oSalesOrderToItem30NoteError,
				"(SalesOrderID='1',ItemPosition='30~1~')/Note",
				["(SalesOrderID='1',ItemPosition='30~1~')/GrossAmount"]),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Input id="note" value="{Note}" />\
	<Table growing="true" growingThreshold="2" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				batchNo : 1,
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader([
					oSalesOrderDeliveryStatusAndToItemError,
					oSalesOrderNoteError,
					oSalesOrderToItemsError,
					oSalesOrderToItem10ToProductPriceError,
					oSalesOrderToItem20NoteWarning,
					oSalesOrderToItem30NoteError
				])
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectRequest({
				batchNo : 1,
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers :
					{"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					Note : "Baz",
					ItemPosition : "20~1~",
					SalesOrderID : "1"
				}]
			})
			.expectChange("itemPosition", ["10~0~", "20~1~"])
			.expectChange("note::item", ["Bar", "Baz"])
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/")
			.expectMessage(oSalesOrderToItemsError, "/SalesOrderSet('1')/")
			.expectMessage(oSalesOrderItem10ToProductPriceError, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems")
			.expectMessage(oSalesOrderItem20NoteWarning, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems")
			.expectMessage(oSalesOrderItem30NoteError, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems")
			.expectMessage(oSalesOrderDeliveryStatusAndItemError,
				["/SalesOrderSet('1')/", "/SalesOrderLineItemSet"],
				["/SalesOrderSet('1')/", "/SalesOrderSet('1')/ToLineItems"]);

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			function filterErrors(oMessage) {
				return oMessage.getType() === MessageType.Error;
			}

			// code under test
			oItemsBinding = that.oView.byId("table").getBinding("items");

			return oItemsBinding.requestFilterForMessages(filterErrors);
		}).then(function (oFilter) {
			that.expectRequest({
					batchNo : 2,
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					headers :
						{"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"},
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=2"
						+ "&$filter=(SalesOrderID eq '1' and ItemPosition eq '40~2~')"
						+ " or (SalesOrderID eq '1' and ItemPosition eq '10~0~')"
						+ " or (SalesOrderID eq '1' and ItemPosition eq '30~1~')"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						},
						Note : "Bar",
						ItemPosition : "10~0~",
						SalesOrderID : "1"
					}, {
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='30~1~')"
						},
						Note : "Qux",
						ItemPosition : "30~1~",
						SalesOrderID : "1"
					}]
				})
				.expectChange("itemPosition", "30~1~", 1)
				.expectChange("note::item", "Qux", 1);

			oItemsBinding.filter(oFilter);

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: On an object page a carrier with its flights is displayed. Only a part of the
	// flights are displayed. All of the flights have messages. Use client-side-filtering to filter
	// the flights table by entries with "warning" messages.
	// JIRA: CPOUI5MODELS-106
	QUnit.test("Filter table by items with messages - client side filtering", function (assert) {
		var oItemsBinding,
			oModel = createRMTSampleFlightModel({defaultOperationMode : "Client"}),
			oCarrierToFlight10PriceError = this.createResponseMessage(
				"carrierFlights(carrid='1',connid='10~0~',"
				+ "fldate=datetime'2015-05-30T13:47:26.253')/PRICE"),
			oCarrierToFlight20PriceWarning = this.createResponseMessage(
				"carrierFlights(carrid='1',connid='20~0~',"
				+ "fldate=datetime'2015-06-30T13:47:26.253')/PRICE", undefined, "warning"),
			oFlight10PriceError = cloneODataMessage(oCarrierToFlight10PriceError,
				"(carrid='1',connid='10~0~',fldate=datetime'2015-05-30T13:47:26.253')/PRICE"),
			oFlight20PriceWarning = cloneODataMessage(oCarrierToFlight20PriceWarning,
				"(carrid='1',connid='20~0~',fldate=datetime'2015-06-30T13:47:26.253')/PRICE"),
			sView = '\
<FlexBox binding="{/CarrierCollection(\'1\')}">\
	<Text id="carrierID" text="{carrid}" />\
	<Table growing="true" growingThreshold="1" id="table" items="{\
			path : \'carrierFlights\',\
			parameters : {transitionMessagesOnly : true}\
		}">\
		<Text id="connectionID" text="{connid}" />\
		<Text id="flightDate" text="{\
			path:\'fldate\',\
			type: \'sap.ui.model.odata.type.DateTime\',\
			formatOptions: {style : \'short\', UTC : true}\
		}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectRequest({
				deepPath : "/CarrierCollection('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "CarrierCollection('1')"
			}, {
				__metadata : {uri : "CarrierCollection('1')"},
				carrid : "1"
			}, {
				"sap-message" : getMessageHeader([
					oCarrierToFlight10PriceError,
					oCarrierToFlight20PriceWarning
				])
			})
			.expectChange("carrierID", null)
			.expectChange("carrierID", "1")
			.expectRequest({
				deepPath : "/CarrierCollection('1')/carrierFlights",
				headers :
					{"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"},
				requestUri : "CarrierCollection('1')/carrierFlights"
			}, {
				results : [{
					__metadata : {
						uri : "FlightCollection(carrid='1',connid='10~0~',"
							+ "fldate=datetime'2015-05-30T13:47:26.253')"
					},
					carrid : "1",
					connid : "10~0~",
					fldate : new Date(1432993646253)
				}, {
					__metadata : {
						uri : "FlightCollection(carrid='1',connid='20~0~',"
							+ "fldate=datetime'2015-06-30T13:47:26.253')"
					},
					carrid : "1",
					connid : "20~0~",
					fldate : new Date(1435672046253)
				}]
			})
			.expectChange("connectionID", ["10~0~"])
			.expectChange("flightDate", ["5/30/15, 1:47 PM"])
			.expectMessage(oFlight10PriceError, "/FlightCollection",
				"/CarrierCollection('1')/carrierFlights")
			.expectMessage(oFlight20PriceWarning, "/FlightCollection",
				"/CarrierCollection('1')/carrierFlights");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			oItemsBinding = that.oView.byId("table").getBinding("items");

			// code under test
			return oItemsBinding.requestFilterForMessages(function (oMessage) {
				return oMessage.getType() === MessageType.Warning;
			});
		}).then(function (oFilter) {
			that.expectChange("connectionID", ["20~0~"])
				.expectChange("flightDate", ["6/30/15, 1:47 PM"]);

			oItemsBinding.filter(oFilter);

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Data state of a list binding is up to date after initialization and after a
	// relative list binding changes its context.
	// BCP: 2070113436, 2070134258
	QUnit.test("ODataListBinding: Correct data state after initialization or context switch",
			function (assert) {
		var oModel = createSalesOrdersModelMessageScope(),
			oItemsBinding,
			oSalesOrderToItem10ToProductPriceError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderItem10ToProductPriceError
				= cloneODataMessage(oSalesOrderToItem10ToProductPriceError,
					"(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Table growing="true" growingThreshold="20" id="table" items="{\
			path : \'ToLineItems\',\
			parameters : {transitionMessagesOnly : true},\
			templateShareable : true\
		}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
		<Input id="note::item" value="{Note}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "Foo",
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader([oSalesOrderToItem10ToProductPriceError])
			})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers :
					{"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=20"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					Note : "Bar",
					ItemPosition : "10~0~",
					SalesOrderID : "1"
				}]
			})
			.expectMessage(oSalesOrderItem10ToProductPriceError, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			oItemsBinding = that.oView.byId("table").getBinding("items");

			// data state is up to date after changing the context for undefined to new context
			assert.strictEqual(oItemsBinding.getDataState().getMessages().length, 1);

			return that.waitForChanges(assert);
		}).then(function (oFilter) {
			var oTable = that.oView.byId("table"),
				oBindingInfo = oTable.getBindingInfo("items");

			oTable.unbindAggregation("items");

			assert.strictEqual(oTable.getItems().length, 0);

			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					headers :
						{"sap-message-scope" : "BusinessObject", "sap-messages" : "transientOnly"},
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=20"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						},
						Note : "Bar",
						ItemPosition : "10~0~",
						SalesOrderID : "1"
					}]
				});

			// code under test - rebind the table; consider already available messages
			oTable.bindItems(oBindingInfo);

			return that.waitForChanges(assert);
		}).then(function () {
			oItemsBinding = that.oView.byId("table").getBinding("items");

			// messages returned in the request for the sales order are considered after
			// initializing the binding
			assert.strictEqual(oItemsBinding.getDataState().getMessages().length, 1);

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Create two new entities and reload the collection in the same $batch. Test the
	// successfully creation and the creation of the first entity fails which leads to single error
	// response for the changeset. Ensure that test framework processes the requests as expected.
	// JIRA: CPOUI5MODELS-198
[{
	aExpectedMessages : [],
	aResponses : [{ // 1st create
		data : {
			__metadata : {uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"},
			ItemPosition : "10",
			SalesOrderID : "1"
		},
		statusCode : 201
	}, { // 2nd create
		data : {
			__metadata : {uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')"},
			ItemPosition : "20",
			SalesOrderID : "1"
		},
		statusCode : 201
	}, { // read all items
		results : [{
			__metadata : {uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"},
			ItemPosition : "10",
			Note : "Foo",
			SalesOrderID : "1"
		}, {
			__metadata : {
				uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')"
			},
			ItemPosition : "20",
			Note : "Bar",
			SalesOrderID : "1"
		}]
	}],
	sTitle : "Successfully create 2 entities"
}, {
	aExpectedMessages : [{
		code : "UF0",
		descriptionUrl : "",
		fullTarget : "/SalesOrderSet('1')/ToLineItems('~key~')",
		message : "Internal Server Error",
		persistent : false,
		target : "/SalesOrderLineItemSet('~key~')",
		technical : true,
		type : "Error"
	}, {
		code : "UF0",
		descriptionUrl : "",
		fullTarget : "/SalesOrderSet('1')/ToLineItems('~key~')",
		message : "Internal Server Error",
		persistent : false,
		target : "/SalesOrderLineItemSet('~key~')",
		technical : true,
		type : "Error"
	}],
	aResponses : [createErrorResponse(), undefined/*no response needed*/, {results : []}],
	sTitle : "Create 2 entities with error response"
}].forEach(function (oFixture) {
	QUnit.test("ODataModel#createEntry: " + oFixture.sTitle, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				canonicalRequests : true,
				useBatch : true
			}),
			that = this;

		return this.createView(assert, "", oModel).then(function () {
			var oEventHandlers = {
					requestCompleted : function () {},
					requestFailed : function () {}
				},
				bWithError = oFixture.aExpectedMessages.length > 0;

			that.expectHeadRequest()
				.expectRequest({
					batchNo : 1,
					created : true,
					data : {
						__metadata : {
							type : "gwsample_basic.SalesOrderLineItem"
						}
					},
					deepPath : "/SalesOrderSet('1')/ToLineItems('~key~')",
					method : "POST",
					requestUri : "SalesOrderSet('1')/ToLineItems"
				}, oFixture.aResponses[0])
				.expectRequest({
					batchNo : 1,
					created : true,
					data : {
						__metadata : {
							type : "gwsample_basic.SalesOrderLineItem"
						}
					},
					deepPath : "/SalesOrderSet('1')/ToLineItems('~key~')",
					method : "POST",
					requestUri : "SalesOrderSet('1')/ToLineItems"
				}, oFixture.aResponses[1])
				.expectRequest({
					batchNo : 1,
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					requestUri : "SalesOrderSet('1')/ToLineItems"
				}, oFixture.aResponses[2])
				.expectMessages(oFixture.aExpectedMessages);
			if (bWithError) {
				that.oLogMock.expects("error").twice()
					.withExactArgs("'HTTP request failed' while processing "
							+ "POST SalesOrderSet('1')/ToLineItems",
						JSON.stringify(oFixture.aResponses[0]), sODataModelClassName);
			}

			// don't care about passed arguments
			that.mock(oEventHandlers).expects("requestCompleted").exactly(3);
			that.mock(oEventHandlers).expects("requestFailed").exactly(bWithError ? 2 : 0);
			oModel.attachRequestCompleted(oEventHandlers.requestCompleted);
			oModel.attachRequestFailed(oEventHandlers.requestFailed);

			// code under test
			oModel.createEntry("/SalesOrderSet('1')/ToLineItems", {properties : {}});

			// code under test
			oModel.createEntry("/SalesOrderSet('1')/ToLineItems", {properties : {}});

			oModel.read("/SalesOrderSet('1')/ToLineItems", {groupId : "changes"});

			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Create a new entity and immediately reset all changes or call deleteCreatedEntry.
	// The created entity is deleted and no request is sent.
	// JIRA: CPOUI5MODELS-198
	QUnit.test("ODataModel#createEntry: discard created entity", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			that = this;

		return this.createView(assert, /*sView*/"", oModel).then(function () {
			// code under test
			oModel.createEntry("/SalesOrderSet('1')/ToLineItems", {properties : {}});

			// code under test
			oModel.resetChanges();

			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			var oContext;

			// code under test
			oContext = oModel.createEntry("/SalesOrderSet('1')/ToLineItems", {properties : {}});

			// code under test
			oModel.deleteCreatedEntry(oContext);
		});
	});

	//*********************************************************************************************
	// Scenario: The creation (POST) of a new entity leads to an automatic expand of the given
	// navigation properties (GET) within the same $batch. If the creation fails, the POST request
	// and the corresponding GET request for the expansion of the navigation properties are repeated
	// with the next call of submitBatch.
	// JIRA: CPOUI5MODELS-198
	QUnit.test("createEntry: automatic expand of navigation properties", function (assert) {
		var iBatchNo = 1,
			oCreatedContext,
			oGETRequest = {
				deepPath : "/$~key~",
				requestUri : "$~key~?$expand=ToProduct&$select=ToProduct"
			},
			oModel = createSalesOrdersModelMessageScope({
				canonicalRequests : true,
				useBatch : true
			}),
			oNoteError = this.createResponseMessage("Note"),
			oPOSTRequest = {
				created : true,
				data : {
					__metadata : {
						type : "gwsample_basic.SalesOrderLineItem"
					}
				},
				deepPath : "/SalesOrderSet('1')/ToLineItems('~key~')",
				headers : {"Content-ID": "~key~", "sap-messages": "transientOnly"},
				method : "POST",
				requestUri : "SalesOrderSet('1')/ToLineItems"
			},
			sView = '\
<FlexBox id="productDetails"\
	binding="{path : \'ToProduct\', parameters : {select : \'Name\'}}">\
	<Text id="productName" text="{Name}" />\
</FlexBox>',
			that = this;

		this.expectChange("productName", null);

		return this.createView(assert, sView, oModel).then(function () {
			var oErrorGET = createErrorResponse({message : "GET failed", statusCode : 400}),
				oErrorPOST = createErrorResponse({message : "POST failed", statusCode : 400}),
				bHandlerCalled;

			function fnHandleError (oEvent) {
				var oResponse = oEvent.getParameter("response");

				if (!bHandlerCalled) {
					assert.strictEqual(oResponse.expandAfterCreateFailed, undefined);
					bHandlerCalled = true;
				} else {
					assert.strictEqual(oResponse.expandAfterCreateFailed, true);
					oModel.detachRequestFailed(fnHandleError);
				}
			}

			that.expectHeadRequest()
				.expectRequest(Object.assign({batchNo : iBatchNo}, oPOSTRequest), oErrorPOST)
				.expectRequest(Object.assign({batchNo : iBatchNo}, oGETRequest), oErrorGET)
				.expectMessages([{
					code : "UF0",
					descriptionUrl : "",
					fullTarget : "/SalesOrderSet('1')/ToLineItems('~key~')",
					message : "POST failed",
					persistent : false,
					target : "/SalesOrderLineItemSet('~key~')",
					technical : true,
					type : "Error"
				}, {
					code : "UF0",
					descriptionUrl : "",
					fullTarget : "/$~key~",
					message : "GET failed",
					persistent : false,
					target : "/$~key~",
					technical : true,
					type : "Error"
				}]);

			oModel.attachRequestFailed(fnHandleError);

			// code under test
			oCreatedContext = oModel.createEntry("/SalesOrderSet('1')/ToLineItems", {
				expand : "ToProduct",
				properties : {}
			});

			that.oLogMock.expects("error")
				.withExactArgs("'HTTP request failed' while processing "
						+ "POST SalesOrderSet('1')/ToLineItems",
					JSON.stringify(oErrorPOST), sODataModelClassName);
			that.oLogMock.expects("error")
				.withExactArgs(sinon.match(new RegExp("'HTTP request failed' while processing "
						+ "GET \\$id-\\d*-\\d*\\?\\$expand=ToProduct&\\$select=ToProduct")),
					JSON.stringify(oErrorGET), sODataModelClassName);

			oModel.submitChanges();

			iBatchNo += 1;
			return that.waitForChanges(assert);
		}).then(function () {
			that.expectRequest(Object.assign({batchNo : iBatchNo}, oPOSTRequest), {
					data : {
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"
						},
						ItemPosition : "10",
						SalesOrderID : "1"
					},
					statusCode : 201
				})
				.expectRequest(Object.assign({batchNo : iBatchNo}, oGETRequest), {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"
					},
					ToProduct : {
						__metadata : {uri : "ProductSet(ProductID='P1')"},
						Name : "Product 1",
						ProductID : "P1"
					}
				}, {
					"sap-message" : getMessageHeader([oNoteError])
				})
				.expectMessage(oNoteError,
					"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/",
					"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/");

			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			that.expectChange("productName", "Product 1");

			// code under test
			that.oView.byId("productDetails").setBindingContext(oCreatedContext);

			return that.waitForChanges(assert);
		}).then(function () {
			[
				"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/ToProduct",
				"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/ToProduct",
				"/ProductSet(ProductID='P1')"
			].forEach(function (sPath) {
				var oData = oModel.getObject(sPath, null, {select : "Name"});

				assert.strictEqual(oData.Name, "Product 1", "getObject for " + sPath);
			});

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: The creation (POST) of a new entity leads to an automatic expand of the given
	// navigation properties within the same $batch. Calling resetChanges on the model removes also
	// the GET request for the automatic expansion of the given navigation properties.
	// JIRA: CPOUI5MODELS-198
	QUnit.test("createEntry: abort automatic expand of navigation properties", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			that = this;

		return this.createView(assert, /*sView*/"", oModel).then(function () {
			// code under test
			oModel.createEntry("/SalesOrderSet('1')/ToLineItems", {
				expand : "ToProduct",
				properties : {}
			});

			// code under test
			oModel.resetChanges();
			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: When the creation of a new entry is called on a collection and leads to messages in
	// the response header, the request's deepPath is modified by replacing the generic UID with the
	// responsed entity key predicate. The generic UID must not appear in the message's calculated
	// fullTarget.
	// BCP: 002028376600002197422020
	QUnit.test("createEntry: update deep path with resulting entity", function (assert) {
		var oModel = createSalesOrdersModel({useBatch : true}),
			oNoteError = this.createResponseMessage("Note"),
			sView = '\
<FlexBox id="page">\
	<Input id="note" value="{Note}" />\
</FlexBox>',
			that = this;

		return this.createView(assert, sView, oModel).then(function () {
			that.expectHeadRequest()
				.expectRequest({
					created : true,
					data : {
						__metadata : {
							type : "GWSAMPLE_BASIC.SalesOrderLineItem"
						}
					},
					deepPath : "/SalesOrderLineItemSet('~key~')",
					method : "POST",
					requestUri : "SalesOrderLineItemSet"
				}, {
					data : {
						__metadata : {
							uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"
						},
						ItemPosition : "10",
						Note : "foo",
						SalesOrderID : "1"
					},
					statusCode : 201
				}, {
					location : "/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/SalesOrderLineItemSet"
						+ "(SalesOrderID='1',ItemPosition='10')",
					"sap-message" : getMessageHeader(oNoteError)
				})
				.expectMessage(oNoteError,
					"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')/");

			// code under test
			that.oView.byId("page").setBindingContext(
				oModel.createEntry("/SalesOrderLineItemSet", {properties : {}})
			);
			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			assert.strictEqual(that.oView.byId("note").getValue(), "foo");
			that.checkValueState(assert, "note", "Error", oNoteError.message);
		});
	});

	//*********************************************************************************************
	// Scenario: When the creation of a new entry is called on a navigation property pointing to a
	// collection and leads to messages in the response header, the request's deepPath is modified
	// by replacing the generic UID with the responsed entity key predicate. The generic UID must
	// not appear in the message's calculated fullTarget.
	// BCP: 002028376600002197422020
	QUnit.test("createEntry: update deep path with resulting entity (deep)", function (assert) {
		var oModel = createSalesOrdersModel({refreshAfterChange : false, useBatch : true}),
			oNoteError = this.createResponseMessage("Note"),
			sView = '\
<FlexBox binding="{path : \'/SalesOrderSet(\\\'1\\\')\',\
		parameters : {select : \'SalesOrderID,Note\', expand : \'ToLineItems\'}}">\
	<Text id="noteSalesOrder" text="{Note}" />\
	<Table id="table" items="{path : \'ToLineItems\',\
			parameters : {select : \'ItemPosition,Note,SalesOrderID\'}}">\
		<Text id="itemPosition" text="{ItemPosition}" />\
	</Table>\
</FlexBox>\
<FlexBox id="details">\
	<Input id="noteLineItem" value="{Note}" />\
</FlexBox>',
			that = this;

		this.expectHeadRequest()
			.expectRequest("SalesOrderSet('1')?$select=SalesOrderID%2cNote&$expand=ToLineItems", {
				__metadata : {uri : "SalesOrderSet('1')"},
				Note : "foo",
				SalesOrderID : "1",
				ToLineItems : {
					results : []
				}
			})
			.expectChange("noteSalesOrder", null)
			.expectChange("noteSalesOrder", "foo");

		return this.createView(assert, sView, oModel).then(function () {
			var oNoteErrorCopy = cloneODataMessage(oNoteError,
					"(SalesOrderID='1',ItemPosition='10')/Note");

			that.expectRequest({
					created : true,
					data : {
						__metadata : {
							type : "GWSAMPLE_BASIC.SalesOrderLineItem"
						}
					},
					deepPath : "/SalesOrderSet('1')/ToLineItems('~key~')",
					method : "POST",
					requestUri : "SalesOrderSet('1')/ToLineItems"
				}, {
					data : {
						__metadata : {
							uri : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')"
						},
						ItemPosition : "10~0~",
						Note : "bar",
						SalesOrderID : "1"
					},
					statusCode : 201
				}, {
					location : "/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/SalesOrderSet('1')/ToLineItems"
						+ "(SalesOrderID='1',ItemPosition='10')",
					"sap-message" : getMessageHeader(oNoteError)
				})
				.expectMessage(oNoteErrorCopy, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");

			// code under test
			that.oView.byId("details").setBindingContext(
				oModel.createEntry("ToLineItems", {
					context : that.oView.byId("table").getBindingContext(),
					properties : {}
				})
			);
			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			assert.strictEqual(that.oView.byId("noteLineItem").getValue(), "bar");
			that.checkValueState(assert, "noteLineItem", "Error", oNoteError.message);
		});
	});

	//*********************************************************************************************
	// Scenario: When createEntry is called on a navigation property the deep path of the request is
	// the same as the fullTarget of associated messages.
	// BCP: 002028376600002197422020
	QUnit.test("createEntry: no change of deep path for non-collections", function (assert) {
		var oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}" id="page">\
	<FlexBox id="details">\
		<Input id="name" value="{CompanyName}" />\
	</FlexBox>\
</FlexBox>',
			that = this;

		this.expectHeadRequest()
			.expectRequest("SalesOrderSet('1')", {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			})
			.expectChange("name", null)
			.expectChange("name", undefined);

		return this.createView(assert, sView, oModel).then(function () {
			var oCompanyNameError = that.createResponseMessage("CompanyName");

			that.expectRequest({
					created : true,
					data : {
						__metadata : {
							type : "GWSAMPLE_BASIC.BusinessPartner"
						}
					},
					deepPath : "/SalesOrderSet('1')/ToBusinessPartner",
					method : "POST",
					requestUri : "SalesOrderSet('1')/ToBusinessPartner"
				}, {
					data : {
						__metadata : {
							uri : "/BusinessPartnerSet('BP1')"
						},
						BusinessPartnerID : "BP1",
						CompanyName : "SAP"
					},
					statusCode : 201
				}, {
					location : "/sap/opu/odata/IWBEP/GWSAMPLE_BASIC/BusinessPartnerSet('BP1')",
					"sap-message" : getMessageHeader(oCompanyNameError)
				})
				.expectChange("name", "SAP")
				.expectMessage(oCompanyNameError, "/BusinessPartnerSet('BP1')/",
					"/SalesOrderSet('1')/ToBusinessPartner/");

			// code under test
			that.oView.byId("details").setBindingContext(
				oModel.createEntry("ToBusinessPartner", {
					context : that.oView.byId("page").getBindingContext(),
					properties : {}
				})
			);
			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Read an entity (SalesOrderLineItem) with an expand on a 0..1 navigation property
	// (ToProduct) plus an expand of a second 0..1 navigation property from the first one
	// (ToProduct/ToSupplier). Element bindings on the second navigation property have a valid
	// context (not null), so that relative bindings underneath hold data as expected.
	// BCP: 2070126588
	//TODO use testViewStart to shorten test
	QUnit.test("BCP 2070126588: binding to nested 0..1 navigation property", function (assert) {
		var sView = '\
<FlexBox id="objectPage" binding="{\
path : \'/SalesOrderLineItemSet(SalesOrderID=\\\'0500000005\\\',ItemPosition=\\\'0000000010\\\')\',\
parameters : {expand : \'ToProduct,ToProduct/ToSupplier\',\
	select : \'SalesOrderID,ItemPosition,ToProduct/ProductID,\
ToProduct/ToSupplier/BusinessPartnerID\'}}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
	<Text id="itemPosition" text="{ItemPosition}" />\
	<FlexBox binding="{path : \'ToProduct\', parameters : {expand : \'ToSupplier\',\
			select : \'ProductID,ToSupplier/BusinessPartnerID\'}}">\
		<Text id="productID" text="{ProductID}" />\
		<FlexBox binding="{path : \'ToSupplier\', parameters : {select : \'BusinessPartnerID\'}}">\
			<Text id="businessPartnerID" text="{BusinessPartnerID}" />\
		</FlexBox>\
	</FlexBox>\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderLineItemSet"
					+ "(SalesOrderID='0500000005',ItemPosition='0000000010')"
					+ "?$expand=ToProduct%2cToProduct%2fToSupplier"
					+ "&$select=SalesOrderID%2cItemPosition%2cToProduct%2fProductID"
						+ "%2cToProduct%2fToSupplier%2fBusinessPartnerID", {
				SalesOrderID : "0500000005",
				ItemPosition : "0000000010",
				ToProduct : {
					__metadata : {
						uri : "/sap/opu/odata/sap/GWSAMPLE_BASIC/ProductSet('HT-1500')"
					},
					ProductID : "HT-1500",
					ToSupplier : {
						__metadata : {
							uri : "/sap/opu/odata/sap/GWSAMPLE_BASIC"
								+ "/BusinessPartnerSet('0100000069')"
						},
						BusinessPartnerID : "0100000069"
					}
				}
			})
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "0500000005")
			.expectChange("itemPosition", null)
			.expectChange("itemPosition", "0000000010")
			.expectChange("productID", null)
			.expectChange("productID", "HT-1500")
			.expectChange("businessPartnerID", null)
			.expectChange("businessPartnerID", "0100000069");

		return this.createView(assert, sView).then(function () {
			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: UI5 messages are not created with a target for unbound transition messages that are
	// sent without a target. The empty ("") target leads still to a target creation for UI5
	// messages.
	// JIRA: CPOUI5MODELS-153
["", undefined].forEach(function (sTarget) {
	[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	var sTitle = "Messages: unbound transition messages; target = '" + sTarget + "'; scope = "
			+ sMessageScope;

	QUnit.test(sTitle, function (assert) {
		var bIsBusinessObject = sMessageScope === MessageScope.BusinessObject,
			oErrorWithoutTarget = this.createResponseMessage(sTarget, undefined, undefined, true),
			bHasTarget = sTarget !== undefined || !bIsBusinessObject,
			sExpectedTarget = bHasTarget ? "/SalesOrderSet('1')" : "",
			oModel = createSalesOrdersModelMessageScope(),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="note" value="{Note}" />\
</FlexBox>';

		this.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : bIsBusinessObject ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				SalesOrderID : "1",
				Note : "Foo"
			}, {
				"sap-message" : getMessageHeader(oErrorWithoutTarget)
			})
			.expectChange("note", null)
			.expectChange("note", "Foo")
			.expectMessages([{
				code : "code-0",
				fullTarget : sExpectedTarget,
				message : "message-0",
				persistent : true,
				target : sExpectedTarget,
				type : MessageType.Error
			}]);

		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel);
	});
	});
});

	//*********************************************************************************************
	// Scenario: The OData response of an updated entity contains changed __metadata (new ETag).
	// This must not lead to pending changes.
	// BCP: 2070060665
	QUnit.test("BCP 2070060665: Ignore __metadata while updating the changed entities",
			function (assert) {
		var oModel = createSalesOrdersModel({refreshAfterChange : false, useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="note" value="{Note}" />\
</FlexBox>',
			that = this;

		this.expectHeadRequest()
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {
					etag : "W/\"2020-05-19T08:08:58.312Z\"",
					uri : "SalesOrderSet('1')"
				},
				Note : "Foo",
				SalesOrderID : "1"
			})
			.expectChange("note", null)
			.expectChange("note", "Foo");

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					data : {
						__metadata : {
							etag : "W/\"2020-05-19T08:08:58.312Z\"",
							uri : "SalesOrderSet('1')"
						},
						Note : "Bar"
					},
					deepPath : "/SalesOrderSet('1')",
					headers : {
						"If-Match" : "W/\"2020-05-19T08:08:58.312Z\""
					},
					key : "SalesOrderSet('1')",
					method : "MERGE",
					requestUri : "SalesOrderSet('1')"
				}, {
					data : {
						__metadata : {
							etag : "W/\"2020-05-19T08:09:00.146Z\"",
							uri : "SalesOrderSet('1')"
						},
						Note : "Bar",
						SalesOrderID : "1"
					},
					headers : {etag : "W/\"2020-05-19T08:09:00.146Z\""},
					statusCode : 200
				})
				.expectChange("note", "Bar");

			// code under test
			oModel.setProperty("/SalesOrderSet('1')/Note", "Bar");
			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			assert.deepEqual(oModel.getPendingChanges(), {});
		});
	});

	//*********************************************************************************************
	// Scenario: Use latest ETag when sending a change request for an entity. Modify a property of
	// an entity and submit the changes. Before the request comes back, modify the property again
	// but wait with the submit until the response of the first modification is processed. The ETag
	// of that response has to be used when sending the second modification to the backend.
	// BCP: 2080271261
	QUnit.test("BCP 2080271261: Use latest ETag when sending a request", function (assert) {
		var oModel = createSalesOrdersModel({refreshAfterChange : false, useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="note" value="{Note}" />\
</FlexBox>',
			that = this;

		this.expectHeadRequest()
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {
					etag : "InitialETag",
					uri : "SalesOrderSet('1')"
				},
				Note : "Foo",
				SalesOrderID : "1"
			})
			.expectChange("note", null)
			.expectChange("note", "Foo");

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					data : {
						__metadata : {
							etag : "InitialETag",
							uri : "SalesOrderSet('1')"
						},
						Note : "Bar"
					},
					deepPath : "/SalesOrderSet('1')",
					headers : {
						"If-Match" : "InitialETag"
					},
					key : "SalesOrderSet('1')",
					method : "MERGE",
					requestUri : "SalesOrderSet('1')"
				}, NO_CONTENT, {
					etag : "ETagAfter1stModification"
				})
				.expectChange("note", "Bar");

			// code under test
			oModel.setProperty("/SalesOrderSet('1')/Note", "Bar");
			oModel.submitChanges();

			that.expectChange("note", "Baz");

			// code under test do a second modification but do not yet submit the change
			oModel.setProperty("/SalesOrderSet('1')/Note", "Baz");

			return that.waitForChanges(assert);
		}).then(function () {
			that.expectRequest({
					data : {
						__metadata : {
							etag : "ETagAfter1stModification",
							uri : "SalesOrderSet('1')"
						},
						Note : "Baz"
					},
					deepPath : "/SalesOrderSet('1')",
					headers : {
						"If-Match" : "ETagAfter1stModification"
					},
					key : "SalesOrderSet('1')",
					method : "MERGE",
					requestUri : "SalesOrderSet('1')"
				}, NO_CONTENT, {
					etag : "ETagAfter2ndModification"
				});

			// code under test
			oModel.submitChanges();

			return that.waitForChanges(assert);
		}).then(function () {
			assert.strictEqual(oModel.getObject("/SalesOrderSet('1')").__metadata.etag,
				"ETagAfter2ndModification");
		});
	});

	//*********************************************************************************************
	// Scenario: Child messages are cleared when an entity is removed.
	// BCP: 2070222122
	// JIRA: CPOUI5MODELS-79
[MessageScope.BusinessObject, MessageScope.RequestedObjects].forEach(function (sMessageScope) {
	var sTitle = "BCP 2070222122: cleanup child messages for #remove, scope: " + sMessageScope;

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oSalesOrderNoteError = this.createResponseMessage("Note"),
			oSalesOrderToItem10ToProductPriceError = this.createResponseMessage(
				"ToLineItems(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			oSalesOrderItem10ToProductPriceError
				= cloneODataMessage(oSalesOrderToItem10ToProductPriceError,
					"(SalesOrderID='1',ItemPosition='10~0~')/ToProduct/Price"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Text id="salesOrderID" text="{SalesOrderID}" />\
</FlexBox>',
			bWithMessageScope = sMessageScope === MessageScope.BusinessObject,
			that = this;

		this.expectHeadRequest(bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {})
			.expectRequest({
				batchNo : 1,
				deepPath : "/SalesOrderSet('1')",
				headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			}, {
				"sap-message" : getMessageHeader([
					oSalesOrderNoteError,
					oSalesOrderToItem10ToProductPriceError
				])
			})
			.expectChange("salesOrderID", null)
			.expectChange("salesOrderID", "1")
			.expectMessage(oSalesOrderNoteError, "/SalesOrderSet('1')/")
			.expectMessage(oSalesOrderItem10ToProductPriceError, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems");

		oModel.setMessageScope(sMessageScope);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					batchNo : 2,
					deepPath : "/SalesOrderSet('1')",
					headers : bWithMessageScope ? {"sap-message-scope" : "BusinessObject"} : {},
					method : "DELETE",
					requestUri : "SalesOrderSet('1')"
				}, {})
				.expectChange("salesOrderID", undefined)
				.expectMessages([]);

			if (!bWithMessageScope) {
				that.expectMessage(oSalesOrderItem10ToProductPriceError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems");
			}

			// code under test
			oModel.remove("/SalesOrderSet('1')");
			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Messages with multiple targets are visualized at controls that are bound against
	// one the the messages' targets. The lifecycle of multi-targets messages is correct, i.e.
	// a new messages where one target matches the target of an existing messages leads to removal
	// of this existing message.
	// JIRA: CPOUI5MODELS-197
	QUnit.test("Messages with multiple targets: value state and lifecycle", function (assert) {
		var oModel = createSalesOrdersModel(),
			oMsgNoteAndGrossAmount = this.createResponseMessage(["Note", "GrossAmount"], "Foo",
				"warning"),
			oMsgGrossAmountAndLifecycleStatus = this.createResponseMessage(
				["Note", "LifecycleStatusDescription"], "Bar", "error"),
			that = this,
			sView = '\
<FlexBox id="objectPage" binding="{/SalesOrderSet(\'1\')}">\
	<Input id="Note" value="{Note}" />\
	<Input id="GrossAmount" value="{GrossAmount}" />\
	<Input id="LifecycleStatusDescription" value="{LifecycleStatusDescription}" />\
</FlexBox>';

		this.expectRequest("SalesOrderSet('1')", {
				GrossAmount : "GrossAmount A",
				LifecycleStatusDescription : "LifecycleStatusDescription A",
				Note : "Note A"
			}, {"sap-message" : getMessageHeader(oMsgNoteAndGrossAmount)})
			.expectChange("Note", null)
			.expectChange("Note", "Note A")
			.expectChange("GrossAmount", null)
			.expectChange("GrossAmount", "GrossAmount A")
			.expectChange("LifecycleStatusDescription", null)
			.expectChange("LifecycleStatusDescription", "LifecycleStatusDescription A")
			.expectMessage(oMsgNoteAndGrossAmount, "/SalesOrderSet('1')/");

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			return Promise.all([
				that.checkValueState(assert, "Note", "Warning", "Foo"),
				that.checkValueState(assert, "GrossAmount", "Warning", "Foo"),
				that.checkValueState(assert, "LifecycleStatusDescription", "None", ""),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			that.expectRequest("SalesOrderSet('1')", {
					GrossAmount : "GrossAmount A",
					LifecycleStatusDescription : "LifecycleStatusDescription A",
					Note : "Note A"
				}, {"sap-message" : getMessageHeader(oMsgGrossAmountAndLifecycleStatus)})
				.expectMessage(oMsgGrossAmountAndLifecycleStatus, "/SalesOrderSet('1')/",
					undefined, /*bResetMessages*/ true);

			// code under test: refresh => new multi-target message removes old one
			that.oView.byId("objectPage").getObjectBinding().refresh();

			return that.waitForChanges(assert);
		}).then(function () {
			return Promise.all([
				that.checkValueState(assert, "Note", "Error", "Bar"),
				that.checkValueState(assert, "GrossAmount", "None", ""),
				that.checkValueState(assert, "LifecycleStatusDescription", "Error", "Bar")
			]);
		});
	});

	//*********************************************************************************************
	// The result of a FunctionImport returning a collection can be accessed via $result relative
	// to the returned context.
	// BCP: 2170085431, 2170084065
[
	{functionName : "allUserAssignmentsGET", method : "GET"},
	{functionName : "allUserAssignmentsPOST", method : "POST"}
].forEach(function (oFixture) {
	var sTitle = "ODataModel#callFunction: bind result ($result) to a list, using method "
			+ oFixture.method + " " + oFixture.functionName;

	QUnit.test(sTitle, function (assert) {
		var oFunctionHandle, fnResolve,
			oModel = createSpecialCasesModel({tokenHandling : false, useBatch : true}),
			oRequestPromise = new Promise(function (resolve) {
				fnResolve = resolve;
			}),
			sView = '\
<t:Table id="table" rows="{path : \'$result\', templateShareable : true}" visibleRowCount="2">\
	<Text id="userId" text="{UserId}" />\
</t:Table>',
			that = this;

		this.expectValue("userId", ["", ""]);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					"deepPath": "/" + oFixture.functionName,
					"headers": {},
					"method": oFixture.method,
					"requestUri": oFixture.functionName
				}, oRequestPromise);

			// code under test
			oFunctionHandle = oModel.callFunction("/" + oFixture.functionName,
				{method : oFixture.method});

			return oFunctionHandle.contextCreated();
		}).then(function (oContext) {
			var oTable = that.oView.byId("table"),
				oResponse = {
					statusCode : 200,
					data : {
						results : [{
							__metadata : {uri : "UserAssignments('User1')"},
							UserId : "User1"
						}, {
							__metadata : {uri : "UserAssignments('User2')"},
							UserId : "User2"
						}]
					}
				};

			that.oLogMock.expects("error").withExactArgs(sinon.match(function (sError) {
				return sError.startsWith("List Binding is not bound against a list for "
						+ "/allUserAssignments");
			}));

			// code under test
			oTable.setBindingContext(oContext);

			that.expectValue("userId", ["User1", "User2"]);

			// code under test - server data processed
			fnResolve(oResponse);

			return Promise.all([
				oRequestPromise,
				that.waitForChanges(assert)
			]);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Messages returned by a function import for an entity contained in a relative
	// collection get the correct full target.
	// JIRA: CPOUI5MODELS-230
	QUnit.test("Messages: function import for relative list entry; w/ location", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oNoteError = this.createResponseMessage("('1')/Note"),
			oToItem10NoteError = this.createResponseMessage(
				"('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/Note"),
			oItem10NoteError = cloneODataMessage(oToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10')/Note"),
			sView = '\
<FlexBox binding="{/BusinessPartnerSet(\'100\')}">\
	<Table items="{ToSalesOrders}">\
		<Text id="soID" text="{SalesOrderID}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"}
			})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')/ToSalesOrders",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')/ToSalesOrders?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}]
			}, {"sap-message" : getMessageHeader([oNoteError, oToItem10NoteError])})
			.expectMessage(oNoteError, "/SalesOrderSet", "/BusinessPartnerSet('100')/ToSalesOrders")
			.expectMessage(oItem10NoteError, "/SalesOrderLineItemSet",
				"/BusinessPartnerSet('100')/ToSalesOrders('1')/ToLineItems")
			.expectChange("soID", "1");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oPromise,
				oGrossAmountError = that.createResponseMessage("GrossAmount"),
				oToItem20QuantityError = that.createResponseMessage(
					"ToLineItems(SalesOrderID='1',ItemPosition='20')/Quantity"),
				oItem20QuantityError = cloneODataMessage(oToItem20QuantityError,
					"(SalesOrderID='1',ItemPosition='20')/Quantity");

			that.expectRequest({
					deepPath : "/SalesOrder_Confirm",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "SalesOrder_Confirm?SalesOrderID='1'"
				}, {
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}, {
					location : "/SalesOrderSrv/SalesOrderSet('1')",
					"sap-message" : getMessageHeader([oGrossAmountError, oToItem20QuantityError])
				})
				.expectMessage(oGrossAmountError, "/SalesOrderSet('1')/",
					"/BusinessPartnerSet('100')/ToSalesOrders('1')/", true)
				.expectMessage(oItem20QuantityError, "/SalesOrderLineItemSet",
					"/BusinessPartnerSet('100')/ToSalesOrders('1')/ToLineItems");

			oPromise = oModel.callFunction("/SalesOrder_Confirm", {
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					SalesOrderID : "1"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: Messages returned by a function import for an entity contained in a relative
	// collection don't get the correct full target without a location header.
	// JIRA: CPOUI5MODELS-230
	QUnit.test("Messages: function import for relative list entry; no location", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oNoteError = this.createResponseMessage("('1')/Note"),
			oToItem10NoteError = this.createResponseMessage(
				"('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/Note"),
			oItem10NoteError = cloneODataMessage(oToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10')/Note"),
			sView = '\
<FlexBox binding="{/BusinessPartnerSet(\'100\')}">\
	<Table items="{ToSalesOrders}">\
		<Text id="soID" text="{SalesOrderID}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"}
			})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')/ToSalesOrders",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')/ToSalesOrders?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}]
			}, {"sap-message" : getMessageHeader([oNoteError, oToItem10NoteError])})
			.expectMessage(oNoteError, "/SalesOrderSet", "/BusinessPartnerSet('100')/ToSalesOrders")
			.expectMessage(oItem10NoteError, "/SalesOrderLineItemSet",
				"/BusinessPartnerSet('100')/ToSalesOrders('1')/ToLineItems")
			.expectChange("soID", "1");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oPromise,
				oGrossAmountError = that.createResponseMessage("GrossAmount"),
				oToItem20QuantityError = that.createResponseMessage(
					"ToLineItems(SalesOrderID='1',ItemPosition='20')/Quantity"),
				oItem20QuantityError = cloneODataMessage(oToItem20QuantityError,
					"(SalesOrderID='1',ItemPosition='20')/Quantity");

			that.expectRequest({
					deepPath : "/SalesOrder_Confirm",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "SalesOrder_Confirm?SalesOrderID='1'"
				}, {
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}, {
					"sap-message" : getMessageHeader([oGrossAmountError, oToItem20QuantityError])
				})
				.expectMessage(oGrossAmountError, "/SalesOrderSet('1')/",
					undefined, true)
				.expectMessage(oItem20QuantityError, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems")
				.expectMessage(oItem10NoteError, "/SalesOrderLineItemSet",
					"/BusinessPartnerSet('100')/ToSalesOrders('1')/ToLineItems");

			oPromise = oModel.callFunction("/SalesOrder_Confirm", {
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					SalesOrderID : "1"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: Messages returned by a function import for a single entity referenced by a
	// navigation property get the correct full target.
	// JIRA: CPOUI5MODELS-230
[false, true].forEach(function (bMultipleOccurrences) {
	var sTitle = "Messages: function import for a navigation property; bMultipleOccurrences = "
			+ bMultipleOccurrences;

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oToBPCompanyNameError = this.createResponseMessage("ToBusinessPartner/CompanyName"),
			oCompanyNameError = cloneODataMessage(oToBPCompanyNameError, "CompanyName"),
			oToProductADescriptionError = this.createResponseMessage(
				"ToBusinessPartner/ToProducts('A')/Description"),
			oProductADescriptionError = cloneODataMessage(oToProductADescriptionError,
				"('A')/Description"),
			sFlexBox = '\
<FlexBox binding="{path : \'ToBusinessPartner\', parameters : {select : \'BusinessPartnerID\'}}">\
	<Text id="bpID0" text="{BusinessPartnerID}" />\
</FlexBox>\
			',
			sView = '\
<FlexBox binding="{path : \'/SalesOrderSet(\\\'1\\\')\', parameters : {\
	expand : \'ToBusinessPartner\', select : \'ToBusinessPartner/BusinessPartnerID\'}}">'
	+ sFlexBox + (bMultipleOccurrences ? sFlexBox.replace("bpID0", "bpID1") : "")
+ '</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')?$expand=ToBusinessPartner"
					+ "&$select=ToBusinessPartner%2fBusinessPartnerID"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1",
				ToBusinessPartner : {
					__metadata : {uri : "BusinessPartnerSet('100')"},
					BusinessPartnerID : "100"
				}
			}, {
				"sap-message" : getMessageHeader([
					oToBPCompanyNameError, oToProductADescriptionError
				])
			})
			.expectMessage(oCompanyNameError, "/BusinessPartnerSet('100')/",
				"/SalesOrderSet('1')/ToBusinessPartner/")
			.expectMessage(oProductADescriptionError, "/ProductSet",
				"/SalesOrderSet('1')/ToBusinessPartner/ToProducts")
			.expectChange("bpID0", null)
			.expectChange("bpID0", "100");

		if (bMultipleOccurrences) {
			this.expectChange("bpID1", null)
				.expectChange("bpID1", "100");
		}

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oPromise,
				oToProductBNameError = that.createResponseMessage("ToProducts('B')/Name"),
				oProductBNameError = cloneODataMessage(oToProductBNameError, "('B')/Name"),
				oWebAddressError = that.createResponseMessage("WebAddress");

			that.expectRequest({
					deepPath : "/BusinessPartner_Refresh",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "BusinessPartner_Refresh?BusinessPartnerID='100'"
				}, {
					__metadata : {uri : "BusinessPartnerSet('100')"},
					BusinessPartnerID : "100"
				}, {
					location : "/SalesOrderSrv/BusinessPartnerSet('100')",
					"sap-message" : getMessageHeader([oWebAddressError, oToProductBNameError])
				})
				.expectMessage(oWebAddressError,
					"/BusinessPartnerSet('100')/",
					"/SalesOrderSet('1')/ToBusinessPartner/", true)
				.expectMessage(oProductBNameError, "/ProductSet",
					"/SalesOrderSet('1')/ToBusinessPartner/ToProducts");

			oPromise = oModel.callFunction("/BusinessPartner_Refresh", {
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					BusinessPartnerID : "100"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});
});

	//*********************************************************************************************
	//TODO: Scenario: Messages returned by a function import for a different single entity
	// referenced by a navigation property does not yet get the correct full target. With feature
	// described in CPOUI5MODELS-230 we correct only the full target of messages for entities that
	// are already on the UI.
	// JIRA: CPOUI5MODELS-230
[false, true].forEach(function (bResultingEntityOnUI) {
	var sTitle = "Messages: function import returns different entity; bResultingEntityOnUI = "
			+ bResultingEntityOnUI;

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oCompanyNameError = this.createResponseMessage("CompanyName"),
			oToProductADescriptionError = this.createResponseMessage("ToProducts('A')/Description"),
			oProductADescriptionError = cloneODataMessage(oToProductADescriptionError,
				"('A')/Description"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<FlexBox binding="{ToBusinessPartner}">\
		<Text id="bpID0" text="{BusinessPartnerID}" />\
	</FlexBox>\
</FlexBox>'
+ (bResultingEntityOnUI
	? '<FlexBox binding="{/ProductSet(\'Z\')}">\
		<FlexBox binding="{ToSupplier}">\
			<Text id="bpID1" text="{BusinessPartnerID}" />\
		</FlexBox>\
	</FlexBox>'
	: ''),
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			})
			.expectChange("bpID0", null)
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToBusinessPartner",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')/ToBusinessPartner"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"},
				BusinessPartnerID : "100"
			}, {"sap-message" : getMessageHeader([oCompanyNameError, oToProductADescriptionError])})
			.expectMessage(oCompanyNameError,
				"/BusinessPartnerSet('100')/",
				"/SalesOrderSet('1')/ToBusinessPartner/")
			.expectMessage(oProductADescriptionError, "/ProductSet",
				"/SalesOrderSet('1')/ToBusinessPartner/ToProducts")
			.expectChange("bpID0", "100");
		if (bResultingEntityOnUI) {
			this.expectRequest({
					deepPath : "/ProductSet('Z')",
					headers : {"sap-message-scope" : "BusinessObject"},
					requestUri : "ProductSet('Z')"
				}, {
					__metadata : {uri : "ProductSet('Z')"},
					ProductID : "Z"
				})
				.expectChange("bpID1", null)
				.expectRequest({
					deepPath : "/ProductSet('Z')/ToSupplier",
					headers : {"sap-message-scope" : "BusinessObject"},
					requestUri : "ProductSet('Z')/ToSupplier"
				}, {
					__metadata : {uri : "BusinessPartnerSet('200')"},
					BusinessPartnerID : "200"
				})
				.expectChange("bpID1", "200");
		}

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oPromise,
				oToProductBNameError = that.createResponseMessage("ToProducts('B')/Name"),
				oProductBNameError = cloneODataMessage(oToProductBNameError, "('B')/Name"),
				oWebAddressError = that.createResponseMessage("WebAddress");

			that.expectRequest({
					deepPath : "/BusinessPartner_Refresh",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "BusinessPartner_Refresh?BusinessPartnerID='100'"
				}, {
					__metadata : {uri : "BusinessPartnerSet('200')"},
					BusinessPartnerID : "200"
				}, {
					location : "/SalesOrderSrv/BusinessPartnerSet('200')",
					"sap-message" : getMessageHeader([oWebAddressError, oToProductBNameError])
				})
				.expectMessage(oWebAddressError, "/BusinessPartnerSet('200')/")
				.expectMessage(oProductBNameError, "/ProductSet",
					"/BusinessPartnerSet('200')/ToProducts");

			oPromise = oModel.callFunction("/BusinessPartner_Refresh", {
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					BusinessPartnerID : "100"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Messages returned by a function import for a single entity with two different deep
	// paths on the UI cannot get automatically a correct full target. So the canonical path is used
	// as full target.
	// JIRA: CPOUI5MODELS-230
	QUnit.test("Messages: function import with same entity twice on UI", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oCompanyNameError = this.createResponseMessage("CompanyName"),
			oToProductADescriptionError = this.createResponseMessage("ToProducts('A')/Description"),
			oProductADescriptionError = cloneODataMessage(oToProductADescriptionError,
				"('A')/Description"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<FlexBox binding="{ToBusinessPartner}">\
		<Text id="bpID0" text="{BusinessPartnerID}" />\
	</FlexBox>\
</FlexBox>\
<FlexBox binding="{/ProductSet(\'Z\')}">\
	<FlexBox binding="{ToSupplier}">\
		<Text id="bpID1" text="{BusinessPartnerID}" />\
	</FlexBox>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			})
			.expectChange("bpID0", null)
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToBusinessPartner",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')/ToBusinessPartner"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"},
				BusinessPartnerID : "100"
			}, {"sap-message" : getMessageHeader([oCompanyNameError, oToProductADescriptionError])})
			// the same entity is returned with a different deep path in another request, so the old
			// messages are removed
			.expectChange("bpID0", "100")
			.expectRequest({
				deepPath : "/ProductSet('Z')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "ProductSet('Z')"
			}, {
				__metadata : {uri : "ProductSet('Z')"},
				ProductID : "Z"
			})
			.expectChange("bpID1", null)
			.expectRequest({
				deepPath : "/ProductSet('Z')/ToSupplier",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "ProductSet('Z')/ToSupplier"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"},
				BusinessPartnerID : "100"
			}, {"sap-message" : getMessageHeader([oCompanyNameError, oToProductADescriptionError])})
			.expectMessage(oCompanyNameError,
				"/BusinessPartnerSet('100')/",
				"/ProductSet('Z')/ToSupplier/")
			.expectMessage(oProductADescriptionError, "/ProductSet",
				"/ProductSet('Z')/ToSupplier/ToProducts")
			.expectChange("bpID1", "100");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oPromise,
				oToProductBNameError = that.createResponseMessage("ToProducts('B')/Name"),
				oProductBNameError = cloneODataMessage(oToProductBNameError, "('B')/Name"),
				oWebAddressError = that.createResponseMessage("WebAddress");

			that.expectRequest({
					deepPath : "/BusinessPartner_Refresh",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "BusinessPartner_Refresh?BusinessPartnerID='100'"
				}, {
					__metadata : {uri : "BusinessPartnerSet('100')"},
					BusinessPartnerID : "100"
				}, {
					location : "/SalesOrderSrv/BusinessPartnerSet('100')",
					"sap-message" : getMessageHeader([oWebAddressError, oToProductBNameError])
				})
				.expectMessage(oProductADescriptionError, "/ProductSet",
					"/ProductSet('Z')/ToSupplier/ToProducts", true)
				.expectMessage(oWebAddressError, "/BusinessPartnerSet('100')/")
				.expectMessage(oProductBNameError, "/ProductSet",
					"/BusinessPartnerSet('100')/ToProducts");

			oPromise = oModel.callFunction("/BusinessPartner_Refresh", {
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					BusinessPartnerID : "100"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: Parameters of a function import are changed after calling ODataModel#callFunction
	// before submitting the changes (#submitChanges). The request URL contains the latest parameter
	// change and the messages get the correct target/fullTarget. Object for parameters does not
	// cause pending changes after the execution of the function import.
	// BCP: 2070289685, 2070333970
	// JIRA: CPOUI5MODELS-230
[
	{method: "GET", functionName : "/SalesOrder_Confirm_GET"},
	{method: "POST", functionName : "/SalesOrder_Confirm"}
].forEach(function (oFixture) {
	var sTitle = "Messages: function import with lazy parameter determination, method="
			+ oFixture.method;

	QUnit.test(sTitle, function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				defaultBindingMode : "TwoWay",
				tokenHandling : false,
				useBatch : true
			}),
			sView = '\
<FlexBox binding="{/BusinessPartnerSet(\'100\')}">\
	<Table items="{ToSalesOrders}">\
		<Text id="soID" text="{SalesOrderID}" />\
	</Table>\
</FlexBox>\
<FlexBox id="form">\
	<Input id="soIDParameter" value="{SalesOrderID}" />\
</FlexBox>',
			that = this;

		this.expectRequest({
				deepPath : "/BusinessPartnerSet('100')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"}
			})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')/ToSalesOrders",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')/ToSalesOrders?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {uri : "SalesOrderSet('42')"},
					SalesOrderID : "42"
				}]
			})
			.expectChange("soID", "42");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return Promise.all([
			oModel.callFunction(oFixture.functionName, {
				groupId : "changes",
				method : oFixture.method,
				refreshAfterChange : false,
				urlParameters : {
					SalesOrderID : "1"
				}
			}).contextCreated(),
			this.createView(assert, sView, oModel)
		]).then(function (aResults) {
			var oRequest = {
					deepPath : oFixture.functionName,
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : oFixture.method,
					requestUri : oFixture.functionName.slice(1) + "?SalesOrderID='42'"
				},
				oWebAddressError = that.createResponseMessage("WebAddress");

			if (oFixture.method === "POST") {
				oRequest.data = undefined;
			}

			that.expectRequest(oRequest, {
					__metadata : {uri : "SalesOrderSet('42')"},
					SalesOrderID : "42"
				}, {
					location : "/SalesOrderSrv/SalesOrderSet('42')",
					"sap-message" : getMessageHeader(oWebAddressError)
				})
				.expectMessage(oWebAddressError, "/SalesOrderSet('42')/",
					"/BusinessPartnerSet('100')/ToSalesOrders('42')/");

			that.oView.byId("form").setBindingContext(aResults[0]);
			that.oView.byId("soIDParameter").setValue("42");

			// code under test
			oModel.submitChanges({groupId : "changes"});

			return that.waitForChanges(assert);
		}).then(function () {
			assert.strictEqual(oModel.hasPendingChanges(true), false);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Parameters of a function import are changed after calling ODataModel#callFunction
	// before submitting the changes (#submitChanges). The request URL contains the latest parameter
	// change and the messages get the correct target/fullTarget. Navigation properties are expanded
	// in the same $batch.
	// JIRA: CPOUI5MODELS-221
	QUnit.test("Messages: function import with expand and lazy parameters", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				defaultBindingMode : "TwoWay",
				tokenHandling : false,
				useBatch : true
			}),
			sView = '\
<FlexBox id="form">\
	<Input id="soIDParameter" value="{SalesOrderID}" />\
</FlexBox>',
			that = this;

		oModel.setMessageScope(MessageScope.BusinessObject);

		return Promise.all([
			oModel.callFunction("/SalesOrder_Confirm", {
				expand : "ToLineItems",
				groupId : "changes",
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					SalesOrderID : "1"
				}
			}).contextCreated(),
			this.createView(assert, sView, oModel)
		]).then(function (aResults) {
			var oWebAddressError = that.createResponseMessage("WebAddress");

			that.expectRequest({
					batchNo : 1,
					data : undefined,
					deepPath : "/SalesOrder_Confirm",
					encodeRequestUri : false,
					headers : {
						"Content-ID": "~key~",
						"sap-message-scope" : "BusinessObject",
						"sap-messages": "transientOnly"
					},
					method : "POST",
					requestUri : "SalesOrder_Confirm?SalesOrderID='42'"
				}, {
					__metadata : {uri : "SalesOrderSet('42')"},
					SalesOrderID : "42"
				}, {
					location : "/SalesOrderSrv/SalesOrderSet('42')"
				})
				.expectRequest({
					batchNo : 1,
					deepPath : "/$~key~",
					headers : {"sap-message-scope" : "BusinessObject"},
					requestUri : "$~key~?$expand=ToLineItems&$select=ToLineItems"
				}, {
					__metadata : {uri : "SalesOrderSet('42')"},
					ToLineItems : {
						results : [{
							__metadata : {
								uri : "SalesOrderLineItemSet(SalesOrderID='42',ItemPosition='10')"
							},
							ItemPosition : "10",
							Note : "ItemNote",
							SalesOrderID : "42"
						}]
					}
				}, {
					"sap-message" : getMessageHeader(oWebAddressError)
				})
				.expectMessage(oWebAddressError, "/SalesOrderSet('42')/");

			that.oView.byId("form").setBindingContext(aResults[0]);
			that.oView.byId("soIDParameter").setValue("42");

			// code under test
			oModel.submitChanges({groupId : "changes"});

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Messages returned by a function import get the correct full target by using the
	// given callback function adjustDeepPath. The framework is not able to determine the correct
	// deep path.
	// JIRA: CPOUI5MODELS-262
	QUnit.test("Messages: function import with callback function", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Table items="{path : \'ToLineItems\', parameters : {transitionMessagesOnly : true}}">\
		<Text id="note" text="{Note}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : {
					"sap-message-scope" : "BusinessObject",
					"sap-messages" : "transientOnly"
				},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					ItemPosition : "10~0~",
					Note : "Foo",
					SalesOrderID : "1"
				}]
			})
			.expectChange("note", "Foo", 0);

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oNoteError = that.createResponseMessage("Note"),
				oPromise;

			that.expectRequest({
					deepPath : "/LineItem_Create",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "LineItem_Create?SalesOrderID='1'"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					ItemPosition : "20~1~",
					Note : "Bar",
					SalesOrderID : "1"
				}, {
					location : "/SalesOrderSrv/"
						+ "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')",
					"sap-message" : getMessageHeader(oNoteError)
				})
				.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					headers : {
						"sap-message-scope" : "BusinessObject",
						"sap-messages" : "transientOnly"
					},
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						},
						ItemPosition : "10~0~",
						Note : "Foo",
						SalesOrderID : "1"
					}, {
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
						},
						ItemPosition : "20~1~",
						Note : "Bar",
						SalesOrderID : "1"
					}]
				})
				.expectChange("note", "Bar", 1)
				.expectMessage(oNoteError,
					"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')/",
					"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='20~1~')/");

			// code under test
			oPromise = oModel.callFunction("/LineItem_Create", {
				adjustDeepPath : function (mParameters) {
					assert.strictEqual(mParameters.deepPath,
						"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')");
					return "/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='20~1~')";
				},
				method : "POST",
				urlParameters : {
					SalesOrderID : "1"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: Messages returned by a function import get the correct full target. The callback
	// function adjustDeepPath verifies that the ODataModel was able to calculate a correct deep
	// path based on given binding information. The callback function is used to overwrite the
	// calculated deep path due to the application's business logic. Exisiting messages for the
	// originally calculated deep path are not removed.
	// JIRA: CPOUI5MODELS-262
	QUnit.test("Messages: function import with callback function overrides calculated deepPath",
			function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oNoteError = this.createResponseMessage("('1')/Note"),
			oToItem10NoteError = this.createResponseMessage(
				"('1')/ToLineItems(SalesOrderID='1',ItemPosition='10')/Note"),
			oItem10NoteError = cloneODataMessage(oToItem10NoteError,
				"(SalesOrderID='1',ItemPosition='10')/Note"),
			sView = '\
<FlexBox binding="{/BusinessPartnerSet(\'100\')}">\
	<Table items="{ToSalesOrders}">\
		<Text id="soID" text="{SalesOrderID}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')"
			}, {
				__metadata : {uri : "BusinessPartnerSet('100')"}
			})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('100')/ToSalesOrders",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('100')/ToSalesOrders?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}]
			}, {"sap-message" : getMessageHeader([oNoteError, oToItem10NoteError])})
			.expectMessage(oNoteError, "/SalesOrderSet", "/BusinessPartnerSet('100')/ToSalesOrders")
			.expectMessage(oItem10NoteError, "/SalesOrderLineItemSet",
				"/BusinessPartnerSet('100')/ToSalesOrders('1')/ToLineItems")
			.expectChange("soID", "1");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oGrossAmountError = that.createResponseMessage("GrossAmount"),
				oPromise,
				oToItem20QuantityError = that.createResponseMessage(
					"ToLineItems(SalesOrderID='1',ItemPosition='20')/Quantity"),
				oItem20QuantityError = cloneODataMessage(oToItem20QuantityError,
					"(SalesOrderID='1',ItemPosition='20')/Quantity");

			that.expectRequest({
					deepPath : "/SalesOrder_Confirm",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "SalesOrder_Confirm?SalesOrderID='1'"
				}, {
					__metadata : {uri : "SalesOrderSet('1')"},
					SalesOrderID : "1"
				}, {
					location : "/SalesOrderSrv/SalesOrderSet('1')",
					"sap-message" : getMessageHeader([oGrossAmountError, oToItem20QuantityError])
				})
				.expectMessage(oGrossAmountError, "/SalesOrderSet('1')/",
					"/BusinessPartnerSet('200')/ToSalesOrders('1')/", true)
				.expectMessage(oItem20QuantityError, "/SalesOrderLineItemSet",
					"/BusinessPartnerSet('200')/ToSalesOrders('1')/ToLineItems")
				// oItem10NoteError is not removed because the value
				// SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10') is not an affected
				// target and prefix match does not match because BusinessPartner is updated to 200
				.expectMessage(oItem10NoteError, "/SalesOrderLineItemSet",
					"/BusinessPartnerSet('100')/ToSalesOrders('1')/ToLineItems");

			oPromise = oModel.callFunction("/SalesOrder_Confirm", {
				adjustDeepPath : function (mParameters) {
					assert.strictEqual(mParameters.deepPath,
						"/BusinessPartnerSet('100')/ToSalesOrders('1')");
					assert.strictEqual(mParameters.response.headers.location,
						"/SalesOrderSrv/SalesOrderSet('1')");
					return "/BusinessPartnerSet('200')/ToSalesOrders('1')";
				},
				method : "POST",
				refreshAfterChange : false,
				urlParameters : {
					SalesOrderID : "1"
				}
			});

			return Promise.all([
				oPromise.contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: With the binding parameter <code>ignoreMessages</code> the application developer
	// can control whether messages are displayed at the control. It works for
	// <code>sap.ui.model.odata.ODataPropertyBinding</code>s and composite bindings containing such
	// bindings.
	// JIRA: CPOUI5MODELS-290
	QUnit.test("ODataPropertyBindings and CompositeBindings: ignoreMessages", function (assert) {
		var oModel = createSalesOrdersModel(),
			oNoteWarning = this.createResponseMessage("Note", "Foo", "warning"),
			sView = '\
<FlexBox id="objectPage" binding="{/SalesOrderSet(\'1\')}">\
	<Input id="Note0" value="{Note}" />\
	<Input id="Note1" value="{path : \'Note\', parameters : {ignoreMessages : false}}" />\
	<Input id="Note2" value="{path : \'Note\', parameters : {ignoreMessages : true}}" />\
	<Input id="Composite0" value="{= ${SalesOrderID} + ${value : \' - \'} + ${Note}}" />\
	<Input id="Composite1" value="{= ${SalesOrderID} + ${value : \' - \'} + ${\
			path : \'Note\',\
			parameters : {ignoreMessages : false}\
		}}" />\
	<Input id="Composite2" value="{= ${SalesOrderID} + ${value : \' - \'} + ${\
			path : \'Note\',\
			parameters : {ignoreMessages : true}\
		}}" />\
	<Input id="Composite3" value="{parts : [\'SalesOrderID\', {value : \'-\'}, {\
			path : \'Note\',\
			parameters : {ignoreMessages : false}\
		}]}" />\
	<Input id="Composite4" value="{parts : [\'SalesOrderID\', {value : \'-\'}, {\
			path : \'Note\',\
			parameters : {ignoreMessages : true}\
		}]}" />\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", {
				Note : "Note",
				SalesOrderID : '1'
			}, {"sap-message" : getMessageHeader(oNoteWarning)})
			.expectChange("Note0", null)
			.expectChange("Note0", "Note")
			.expectChange("Note1", null)
			.expectChange("Note1", "Note")
			.expectChange("Note2", null)
			.expectChange("Note2", "Note")
			.expectMessage(oNoteWarning, "/SalesOrderSet('1')/");

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			assert.strictEqual(that.oView.byId("Composite0").getValue(), "1 - Note");
			assert.strictEqual(that.oView.byId("Composite1").getValue(), "1 - Note");
			assert.strictEqual(that.oView.byId("Composite2").getValue(), "1 - Note");
			assert.strictEqual(that.oView.byId("Composite3").getValue(), "1 - Note");
			assert.strictEqual(that.oView.byId("Composite4").getValue(), "1 - Note");
			return Promise.all([
				that.checkValueState(assert, "Note0", "Warning", "Foo"),
				that.checkValueState(assert, "Note1", "Warning", "Foo"),
				that.checkValueState(assert, "Note2", "None", ""),
				that.checkValueState(assert, "Composite0", "Warning", "Foo"),
				that.checkValueState(assert, "Composite1", "Warning", "Foo"),
				that.checkValueState(assert, "Composite2", "None", ""),
				that.checkValueState(assert, "Composite3", "Warning", "Foo"),
				that.checkValueState(assert, "Composite4", "None", ""),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: With the binding parameter <code>ignoreMessages</code> the application developer
	// can control whether messages are displayed at the control. For
	// <code>sap.ui.model.type.Currency</code> the parameter <code>ignoreMessages</code> is
	// determined automatically based on the format option <code>showMeasure</code>. Manual setting
	// of <code>ignoreMessages</code> wins over automatic determination.
	// JIRA: CPOUI5MODELS-302
	QUnit.test("ignoreMessages for sap.ui.model.type.Currency", function (assert) {
		var oModel = createSalesOrdersModel(),
			oCurrencyCodeWarning = this.createResponseMessage("CurrencyCode", "Foo", "warning"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="Amount0" value="{\
			formatOptions : {showMeasure : false},\
			mode : \'TwoWay\',\
			parts : [{\
				constraints : {precision : 16, scale : 3},\
				path : \'GrossAmount\',\
				type : \'sap.ui.model.odata.type.Decimal\'\
			}, {\
				constraints : {maxLength : 5},\
				path : \'CurrencyCode\',\
				type : \'sap.ui.model.odata.type.String\'\
			}],\
			type : \'sap.ui.model.type.Currency\'\
		}" />\
	<Input id="Amount1" value="{\
			formatOptions : {showMeasure : false},\
			mode : \'TwoWay\',\
			parts : [{\
				constraints : {precision : 16, scale : 3},\
				path : \'GrossAmount\',\
				type : \'sap.ui.model.odata.type.Decimal\'\
			}, {\
				constraints : {maxLength : 5},\
				parameters : {ignoreMessages : false},\
				path : \'CurrencyCode\',\
				type : \'sap.ui.model.odata.type.String\'\
			}],\
			type : \'sap.ui.model.type.Currency\'\
		}" />\
	<Input id="Amount2" value="{\
			formatOptions : {showMeasure : false},\
			mode : \'TwoWay\',\
			parts : [{\
				constraints : {precision : 16, scale : 3},\
				path : \'GrossAmount\',\
				type : \'sap.ui.model.odata.type.Decimal\'\
			}, {\
				constraints : {maxLength : 5},\
				parameters : {ignoreMessages : true},\
				path : \'CurrencyCode\',\
				type : \'sap.ui.model.odata.type.String\'\
			}],\
			type : \'sap.ui.model.type.Currency\'\
		}" />\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", {
				CurrencyCode : "JPY",
				GrossAmount : "12345",
				SalesOrderID : "1"
			}, {"sap-message" : getMessageHeader(oCurrencyCodeWarning)})
			// change event for each part of the composite type
			.expectChange("Amount0", "12,345.00")
			.expectChange("Amount0", "12,345")
			.expectChange("Amount1", "12,345.00")
			.expectChange("Amount1", "12,345")
			.expectChange("Amount2", "12,345.00")
			.expectChange("Amount2", "12,345")
			.expectMessage(oCurrencyCodeWarning, "/SalesOrderSet('1')/");

		// code under test
		return this.createView(assert, sView, oModel).then(function () {
			return Promise.all([
				that.checkValueState(assert, "Amount0", "None", ""),
				that.checkValueState(assert, "Amount1", "Warning", "Foo"),
				that.checkValueState(assert, "Amount2", "None", ""),
				that.waitForChanges(assert)
			]);
		});
	});

	//**********************************************************************************************
	// Scenario: In a master-detail scenario there are two requests in one batch, one for the list
	// (entity set) and one for the details (an entity of this set). If the first request responds
	// with a technical error (e.g. incorrect $select leads to 404 'Not Found'), the second request
	// should not clear the received messages, even though they are identified as the same entity.
	// The linked BCP ticket differs from the described scenario but for message processing leads to
	// the same issue.
	// BCP: 2070217402
	// JIRA: CPOUI5MODELS-250
	QUnit.test("Messages: Handle technical messages as persistent", function (assert) {
		var oErrorMessage = createErrorResponse({message : "Not Found", statusCode : 404}),
			oModel = createSalesOrdersModel({
				persistTechnicalMessages : true,
				useBatch : true
			}),
			sView = '\
<Table items="{path : \'/SalesOrderSet\', parameters : {select : \'foo\'}}">\
	<Text text="{SalesOrderID}" />\
</Table>\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="idNote" value="{Note}" />\
</FlexBox>';

		this.expectHeadRequest()
			.expectRequest({
				batchNo : 1,
				deepPath : "/SalesOrderSet",
				requestUri : "SalesOrderSet?$skip=0&$top=100&$select=foo"
			}, oErrorMessage)
			.expectMessages([{
				code : "UF0",
				descriptionUrl : "",
				fullTarget : "",
				message : "Not Found",
				persistent : true,
				target : "",
				technical : true,
				type : "Error"
			}])
			.expectRequest({
				batchNo : 1,
				deepPath : "/SalesOrderSet('1')",
				requestUri : "SalesOrderSet('1')"
			}, {
				Note : "bar"
			})
			.expectChange("idNote", null)
			.expectChange("idNote", "bar");

		this.oLogMock.expects("error")
			.withExactArgs("'HTTP request failed' while processing "
					+ "GET SalesOrderSet?$skip=0&$top=100&$select=foo",
				JSON.stringify(oErrorMessage), sODataModelClassName);

		return this.createView(assert, sView, oModel);
	});

	//**********************************************************************************************
	// Scenario: Allow changing of persistTechnicalMessages on model after instantiation.
	// JIRA: CPOUI5MODELS-344
[true, undefined, false].forEach(function (bPersistTechnicalMessages) {
	var sTitle = "Messages: Change persistTechnicalMessages after instantiation, "
		+ "bPersistTechnicalMessages=" + bPersistTechnicalMessages;

	QUnit.test(sTitle, function (assert) {
		var oErrorMessage = createErrorResponse({message : "Not Found", statusCode : 404}),
			oModel = createSalesOrdersModel({
				persistTechnicalMessages : bPersistTechnicalMessages,
				tokenHandling : false,
				useBatch : true
			}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="idNote" value="{Note}" />\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", oErrorMessage)
			.expectMessages([{
				code : "UF0",
				descriptionUrl : "",
				fullTarget : bPersistTechnicalMessages ? "" : "/SalesOrderSet('1')",
				message : "Not Found",
				persistent : !!bPersistTechnicalMessages,
				target : bPersistTechnicalMessages ? "" : "/SalesOrderSet('1')",
				technical : true,
				type : "Error"
			}])
			.expectChange("idNote", null);

		this.oLogMock.expects("error")
			.withExactArgs("'HTTP request failed' while processing GET SalesOrderSet('1')",
				JSON.stringify(oErrorMessage), sODataModelClassName);

		return this.createView(assert, sView, oModel).then(function () {
			oErrorMessage = createErrorResponse({message : "Not Found", statusCode : 404});

			that.expectRequest("SalesOrderSet('1')", oErrorMessage)
				.expectMessages([{
					code : "UF0",
					descriptionUrl : "",
					fullTarget : bPersistTechnicalMessages ? "" : "/SalesOrderSet('1')",
					message : "Not Found",
					persistent : !!bPersistTechnicalMessages,
					target : bPersistTechnicalMessages ? "" : "/SalesOrderSet('1')",
					technical : true,
					type : "Error"
				}, {
					code : "UF0",
					descriptionUrl : "",
					fullTarget : !bPersistTechnicalMessages ? "" : "/SalesOrderSet('1')",
					message : "Not Found",
					persistent : !bPersistTechnicalMessages,
					target : !bPersistTechnicalMessages ? "" : "/SalesOrderSet('1')",
					technical : true,
					type : "Error"
				}])
				.expectChange("idNote", null);

			that.oLogMock.expects("error")
				.withExactArgs("'HTTP request failed' while processing GET SalesOrderSet('1')",
					JSON.stringify(oErrorMessage), sODataModelClassName);

			if (bPersistTechnicalMessages !== undefined) {
				that.oLogMock.expects("warning")
					.withExactArgs("The flag whether technical messages should always be treated as"
						+ " persistent has been overwritten to " + !bPersistTechnicalMessages,
						undefined, sODataModelClassName);
			}

			// code under test
			oModel.setPersistTechnicalMessages(!bPersistTechnicalMessages);
			oModel.refresh(true);

			return that.waitForChanges(assert);
		});
	});
});

	//*********************************************************************************************
	// Scenario: Root entity returns a message for a *:0..1 navigation property which is
	// <code>null</code>. The data for the navigation property is requested in an own request.
	// The GET request for the navigation property returns a 204 No Content and does not have any
	// messages. The message returned in the request for the root object must not be removed.
	// BCP: 2080337477
	// JIRA: CPOUI5MODELS-339
	QUnit.test("Messages: GET returns 204 No Content", function (assert) {
		var oBusinessPartnerError = this.createResponseMessage("ToBusinessPartner"),
			oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<FlexBox binding="{ToBusinessPartner}">\
		<Text id="id" text="{BusinessPartnerID}" />\
	</FlexBox>\
</FlexBox>';

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			}, {"sap-message" : getMessageHeader(oBusinessPartnerError)})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToBusinessPartner",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')/ToBusinessPartner"
			}, NO_CONTENT
			/* we expect a NO_CONTENT response to have no messages and explicitly ignore them! */)
			.expectChange("id", null)
			.expectMessage(oBusinessPartnerError, "/SalesOrderSet('1')/");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			assert.strictEqual(oModel.getObject("/SalesOrderSet('1')/ToBusinessPartner"), null);
		});
	});

	//*********************************************************************************************
	// Scenario: A function import returning a collection of entities may contain messages for the
	// returned entities. Old messages for the entities returned by the function call are properly
	// updated. Messages for other entities of the same entity set are kept untouched.
	// JIRA: CPOUI5MODELS-287
[
	"/BusinessPartner_Alternatives",
	"/BusinessPartner_Alternatives_ReturnType"
].forEach(function (sFunctionName) {
	var sTitle = "Messages: function import returning a collection for different entities;"
		+ " messages are updated only for returned entities: " + sFunctionName;

	QUnit.test(sTitle, function (assert) {
		var oCompanyNameError1 = this.createResponseMessage("CompanyName"),
			oCompanyNameError2 = this.createResponseMessage("CompanyName"),
			oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oToProductADescriptionError1
				= this.createResponseMessage("ToProducts('A')/Description"),
			oProductADescriptionError1 = cloneODataMessage(oToProductADescriptionError1,
				"('A')/Description"),
			oToProductADescriptionError2
				= this.createResponseMessage("ToProducts('B')/Description"),
			oProductADescriptionError2 = cloneODataMessage(oToProductADescriptionError2,
				"('B')/Description"),
			sView = '\
<FlexBox binding="{/BusinessPartnerSet(\'1\')}">\
	<Input id="companyName1" value="{CompanyName}" />\
</FlexBox>\
<FlexBox binding="{/BusinessPartnerSet(\'2\')}">\
	<Input id="companyName2" value="{CompanyName}" />\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('1')"
			}, {
				__metadata : {uri : "BusinessPartnerSet('1')"},
				BusinessPartnerID : "1",
				CompanyName : "company1"
			}, {"sap-message" : getMessageHeader([
					oCompanyNameError1,
					oToProductADescriptionError1
				])})
			.expectRequest({
				deepPath : "/BusinessPartnerSet('2')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "BusinessPartnerSet('2')"
			}, {
				__metadata : {uri : "BusinessPartnerSet('2')"},
				BusinessPartnerID : "2",
				CompanyName : "company2"
			}, {"sap-message" : getMessageHeader([
					oCompanyNameError2,
					oToProductADescriptionError2
				])})
			.expectChange("companyName1", null)
			.expectChange("companyName2", null)
			.expectChange("companyName1", "company1")
			.expectChange("companyName2", "company2")
			.expectMessage(oCompanyNameError1, "/BusinessPartnerSet('1')/")
			.expectMessage(oProductADescriptionError1, "/ProductSet",
				"/BusinessPartnerSet('1')/ToProducts")
			.expectMessage(oCompanyNameError2, "/BusinessPartnerSet('2')/")
			.expectMessage(oProductADescriptionError2, "/ProductSet",
				"/BusinessPartnerSet('2')/ToProducts");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oCompanyNameError2_Update = that.createResponseMessage("('2')/CompanyName"),
				oToProductADescriptionError2
					= that.createResponseMessage("('2')/ToProducts('B')/Description"),
				oProductADescriptionError2 = cloneODataMessage(oToProductADescriptionError2,
					"('B')/Description");

			that.expectRequest({
					deepPath : sFunctionName,
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : sFunctionName.slice(1) + "?BusinessPartnerID='1'"
				}, {
					results : [{
						__metadata : {uri : "BusinessPartnerSet('2')"},
						BusinessPartnerID : "2",
						CompanyName : "companyName2New"
					}]
				}, {
					"sap-message" : getMessageHeader([
						oCompanyNameError2_Update,
						oToProductADescriptionError2
					])
				})
				.expectChange("companyName2", "companyName2New")
				.expectMessage(oCompanyNameError2_Update, "/BusinessPartnerSet", undefined, true)
				.expectMessage(oProductADescriptionError2, "/ProductSet",
					"/BusinessPartnerSet('2')/ToProducts")
				.expectMessage(oCompanyNameError1, "/BusinessPartnerSet('1')/")
				.expectMessage(oProductADescriptionError1, "/ProductSet",
					"/BusinessPartnerSet('1')/ToProducts");

			return Promise.all([
				oModel.callFunction(sFunctionName, {
					method : "POST",
					refreshAfterChange : false,
					urlParameters : {
						BusinessPartnerID : "1"
					}
				}).contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});
});

	//*********************************************************************************************
	// Scenario: A function import returning a collection of entities with the same parent may
	// contain messages for the returned entities. Old messages for the entities returned by the
	// function call are properly updated. <code>adjustDeepPath</code> is used to overwrite the
	// messages' deep path.
	// JIRA: CPOUI5MODELS-287
	QUnit.test("Messages: function import returning a collection (adjustDeepPath)",
			function (assert) {
		var oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			oQuantityError = this.createResponseMessage(
				"(SalesOrderID='1',ItemPosition='20~1~')/Quantity"),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Table items="{ToLineItems}">\
		<Text id="quantity" text="{Quantity}" />\
	</Table>\
</FlexBox>',
			that = this;

		this.expectHeadRequest({"sap-message-scope" : "BusinessObject"})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')"
			}, {
				__metadata : {uri : "SalesOrderSet('1')"},
				SalesOrderID : "1"
			})
			.expectRequest({
				deepPath : "/SalesOrderSet('1')/ToLineItems",
				headers : {"sap-message-scope" : "BusinessObject"},
				requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
					},
					ItemPosition : "10~0~",
					Quantity : "2",
					SalesOrderID : "1"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
					},
					ItemPosition : "20~1~",
					Quantity : "0",
					SalesOrderID : "1"
				}]
			}, {
				"sap-message" : getMessageHeader(oQuantityError)
			})
			.expectChange("quantity", ["2", "0"])
			.expectMessage(oQuantityError, "/SalesOrderLineItemSet",
				"/SalesOrderSet('1')/ToLineItems");

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, sView, oModel).then(function () {
			var oQuantitySuccess = that.createResponseMessage(
					"(SalesOrderID='1',ItemPosition='20~1~')/Quantity", undefined, "success");

			that.expectRequest({
					deepPath : "/SalesOrder_FixQuantities",
					encodeRequestUri : false,
					headers : {"sap-message-scope" : "BusinessObject"},
					method : "POST",
					requestUri : "SalesOrder_FixQuantities?SalesOrderID='1'"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20~1~')"
						},
						ItemPosition : "20~1~",
						Quantity : "2",
						SalesOrderID : "1"
					}]
				}, {
					"sap-message" : getMessageHeader(oQuantitySuccess)
				})
				.expectChange("quantity", "2", 1)
				.expectMessage(oQuantitySuccess, "/SalesOrderLineItemSet",
					"/SalesOrderSet('1')/ToLineItems", true);

			return Promise.all([
				// code under test
				oModel.callFunction("/SalesOrder_FixQuantities", {
					adjustDeepPath : function (mParameters) {
						assert.strictEqual(mParameters.deepPath, "/SalesOrderLineItemSet");
						return "/SalesOrderSet('1')/ToLineItems";
					},
					method : "POST",
					refreshAfterChange : false,
					urlParameters : {
						SalesOrderID : "1"
					}
				}).contextCreated(),
				that.waitForChanges(assert)
			]);
		});
	});

	//*********************************************************************************************
	// Scenario: Expand navigation properties of the entity returned by a function import call
	// within the same $batch.
	// JIRA: CPOUI5MODELS-221
	QUnit.test("callFunction: expand navigation properties in the same $batch", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				canonicalRequests : true,
				tokenHandling : false,
				useBatch : true
			}),
			sView = '\
<FlexBox id="productDetails">\
	<Text id="productName" text="{Name}" />\
</FlexBox>',
			that = this;

		oModel.setMessageScope(MessageScope.BusinessObject);

		this.expectChange("productName", null);

		return this.createView(assert, sView, oModel).then(function () {
			var oEventHandlers = {
					error : function () {},
					success : function () {}
				},
				oNoteError = that.createResponseMessage("Note"),
				oResponse = {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')"
					},
					ItemPosition : "20",
					SalesOrderID : "1"
				};

			that.mock(oEventHandlers).expects("error").never();
			that.mock(oEventHandlers).expects("success")
				.withExactArgs({
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')"
					},
					ItemPosition : "20",
					SalesOrderID : "1",
					ToProduct : {
						__metadata : {uri : "ProductSet(ProductID='P1')"},
						Name : "Product 1",
						ProductID : "P1"
					}
				}, sinon.match.has("data", oResponse));

			that.expectRequest({
					batchNo : 1,
					deepPath : "/SalesOrderItem_Clone",
					encodeRequestUri : false,
					headers : {
						"Content-ID" : "~key~",
						"sap-message-scope" : "BusinessObject",
						"sap-messages" : "transientOnly"
					},
					method : "POST",
					requestUri : "SalesOrderItem_Clone?ItemPosition='10'&SalesOrderID='1'"
				}, oResponse, {
					location :
						"/SalesOrderSrv/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')"
				})
				.expectRequest({
					batchNo : 1,
					deepPath : "/$~key~",
					headers : {"sap-message-scope" : "BusinessObject"},
					requestUri : "$~key~?$expand=ToProduct&$select=ToProduct"
				}, {
					__metadata : {
						uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')"
					},
					ToProduct : {
						__metadata : {uri : "ProductSet(ProductID='P1')"},
						Name : "Product 1",
						ProductID : "P1"
					}
				}, {
					"sap-message" : getMessageHeader(oNoteError)
				})
				.expectMessage(oNoteError,
					"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')/",
					"/SalesOrderSet('1')/ToLineItems(SalesOrderID='1',ItemPosition='20')/");

			return Promise.all([
				// code under test
				oModel.callFunction("/SalesOrderItem_Clone", {
					adjustDeepPath : function (mParameters) {
						assert.strictEqual(mParameters.deepPath,
							"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')",
							"Deep path adjusted");

						return "/SalesOrderSet('1')"
							+ "/ToLineItems(SalesOrderID='1',ItemPosition='20')";
					},
					error : oEventHandlers.error,
					expand : "ToProduct",
					method : "POST",
					success : oEventHandlers.success,
					urlParameters : {
						ItemPosition : "10",
						SalesOrderID : "1"
					}
				}).contextCreated(),
				that.waitForChanges(assert)
			]).then(function () {
				that.expectChange("productName", "Product 1");

				// code under test
				that.oView.byId("productDetails")
					.bindObject({
						parameters : {select : "Name"},
						path : "/SalesOrderSet('1')"
							+ "/ToLineItems(SalesOrderID='1',ItemPosition='20')/ToProduct"
					});

				return that.waitForChanges(assert);
			});
		});
	});

	//*********************************************************************************************
	// Scenario: Abort a function call with given expand parameter.
	// JIRA: CPOUI5MODELS-221
[true, false].forEach(function (bDeferred) {
	var sTitle = "callFunction: abort function call with given expand parameter; deferred: "
			+ bDeferred;

	QUnit.test(sTitle, function (assert) {
		var oCallFunctionResult,
			oModel = createSalesOrdersModelMessageScope({useBatch : true}),
			that = this;

		oModel.setDeferredGroups(["change", "callFunction"]);

		return this.createView(assert, /*sView*/"", oModel).then(function () {
			oCallFunctionResult = oModel.callFunction("/SalesOrderItem_Clone", {
					adjustDeepPath : function (mParameters) {
						assert.strictEqual(mParameters.deepPath,
							"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')",
							"Deep path adjusted");

						return "/SalesOrderSet('1')"
							+ "/ToLineItems(SalesOrderID='1',ItemPosition='20')";
					},
					expand : "ToProduct",
					groupId : bDeferred ? "callFunction" : undefined,
					method : "POST",
					urlParameters : {
						ItemPosition : "10",
						SalesOrderID : "1"
					}
				});

			if (!bDeferred) {
				// code under test
				oCallFunctionResult.abort();
				oModel.submitChanges();

				return that.waitForChanges(assert);
			}
			// deferred case
			return Promise.all([
				Promise.resolve(), // request object is created async; wait for it
				that.waitForChanges(assert)
			]).then(function () {
				// code under test
				oCallFunctionResult.abort();
				oModel.submitChanges("callFunction");

				return that.waitForChanges(assert);
			});
		});
	});
});

	//*********************************************************************************************
	// Scenario: Function call with given expand parameter fails.
	// JIRA: CPOUI5MODELS-221
	QUnit.test("callFunction: with given expand parameter fails", function (assert) {
		var oModel = createSalesOrdersModelMessageScope({
				canonicalRequests : true,
				tokenHandling : false,
				useBatch : true
			}),
			that = this;

		oModel.setMessageScope(MessageScope.BusinessObject);

		return this.createView(assert, /*sView*/"", oModel).then(function () {
			var oErrorGET = createErrorResponse({message : "GET failed", statusCode : 400}),
				oErrorPOST = createErrorResponse({message : "POST failed", statusCode : 400}),
				oEventHandlers = {
					error : function () {},
					success : function () {}
				};

			that.mock(oEventHandlers).expects("error")
				.withExactArgs(sinon.match({
					message: "HTTP request failed",
					responseText: oErrorPOST.body,
					statusCode: 400,
					statusText: "FAILED"
				}));
			that.mock(oEventHandlers).expects("success").never();
			that.oLogMock.expects("error")
				.withExactArgs("'HTTP request failed' while processing "
						+ "POST SalesOrderItem_Clone?ItemPosition='10'&SalesOrderID='1'",
					JSON.stringify(oErrorPOST), sODataModelClassName);
			that.oLogMock.expects("error")
				.withExactArgs(sinon.match(new RegExp("'HTTP request failed' while processing "
						+ "GET \\$id-\\d*-\\d*\\?\\$expand=ToProduct&\\$select=ToProduct")),
					JSON.stringify(oErrorGET), sODataModelClassName);

			that.expectRequest({
					batchNo : 1,
					deepPath : "/SalesOrderItem_Clone",
					encodeRequestUri : false,
					headers : {
						"Content-ID" : "~key~",
						"sap-message-scope" : "BusinessObject",
						"sap-messages" : "transientOnly"
					},
					method : "POST",
					requestUri : "SalesOrderItem_Clone?ItemPosition='10'&SalesOrderID='1'"
				}, oErrorPOST)
				.expectRequest({
					batchNo : 1,
					deepPath : "/$~key~",
					headers : {"sap-message-scope" : "BusinessObject"},
					requestUri : "$~key~?$expand=ToProduct&$select=ToProduct"
				}, oErrorGET)
				.expectMessages([{
					code : "UF0",
					descriptionUrl : "",
					fullTarget : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')",
					message : "POST failed",
					persistent : false,
					target : "/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10')",
					technical : true,
					type : "Error"
				}, {
					code : "UF0",
					descriptionUrl : "",
					fullTarget : "/$~key~",
					message : "GET failed",
					persistent : false,
					target : "/$~key~",
					technical : true,
					type : "Error"
				}]);

			return Promise.all([
				// code under test
				oModel.callFunction("/SalesOrderItem_Clone", {
					adjustDeepPath : function (mParameters) {
						assert.strictEqual(mParameters.deepPath,
							"/SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='20')",
							"Deep path adjusted");

						return "/SalesOrderSet('1')"
							+ "/ToLineItems(SalesOrderID='1',ItemPosition='20')";
					},
					error : oEventHandlers.error,
					expand : "ToProduct",
					method : "POST",
					success : oEventHandlers.success,
					urlParameters : {
						ItemPosition : "10",
						SalesOrderID : "1"
					}
				}).contextCreated(),
				that.waitForChanges(assert)
			]).then(function () {
				// code under test - function call is not retried
				oModel.submitChanges();

				return that.waitForChanges(assert);
			});
		});
	});

	//*********************************************************************************************
	// Scenario: TreeTable on an ObjectPage bound to a preliminary context.
	// BCP: 2080201638
	QUnit.test("TreeTable with preliminary context", function (assert) {
		var oModel = createAllowanceModel({useBatch : true}),
			sObjectUri = "C_DFS_AllwncReq(guid'fa163e35-93d9-1eda-b19c-c26490674ab4')",
			//use row count 1, as there are 10 null change events otherwise
			sView = '\
<FlexBox binding="{path : \'/C_DFS_AllwncReq(guid\\\'fa163e35-93d9-1eda-b19c-c26490674ab4\\\')\', \
		parameters : {createPreliminaryContext : true, groupId : \'myGroup\'}}">\
	<Text id="reqID" text="{DfsAllwncReqID}" />\
	<t:TreeTable id="table"\
			rows="{path : \'to_AllwncReqToFe\', parameters : \
				{countMode : \'Inline\', groupId : \'myGroup\', usePreliminaryContext : true}}"\
			visibleRowCount="1"\
			visibleRowCountMode="Fixed" \>\
		<Text id="orgID" text="{ForceElementOrgID}" />\
	</t:TreeTable>\
</FlexBox>';

		this.expectHeadRequest()
			.expectRequest({
				batchNo : 1,
				deepPath : "/" + sObjectUri,
				requestUri : sObjectUri
			}, {
				DfsAllwncReqUUID : "fa163e35-93d9-1eda-b19c-c26490674ab4",
				DfsAllwncReqID : "Request ID"
			})
			.expectRequest({
				// TreeTable becomes async so that its GET is not in the same $batch as the 1st GET
				batchNo : 2,
				deepPath : "/" + sObjectUri + "/to_AllwncReqToFe",
				requestUri : sObjectUri + "/to_AllwncReqToFe"
					+ "?$skip=0&$top=101&$inlinecount=allpages&$filter=HierarchyLevel%20le%200"
			}, {
				__count : "1",
				results : [{
					"ForceElementOrgID" : "4711"
					// "HierarchyNode" : "32,FA163E2C58541EDA8E9C92E909255DAF",
					// "HierarchyParentNode" : "",
					// "HierarchyLevel" : 0,
					// "HierarchyDescendantCount" : 0,
					// "DrillDownState" : "collapsed"
				}]
			})
			.expectChange("reqID", null)
			.expectChange("reqID", "Request ID")
			.expectChange("orgID", null, null) //TODO why does this happen?
			.expectChange("orgID", ["4711"]);

		return this.createView(assert, sView, oModel);
	});

	//*********************************************************************************************
	// Scenario: TreeTable using ODataTreeBindingFlat reads automatically an updated entity when
	// binding parameter 'refreshAfterChange' is enabled. With refreshAfterChange set to true, a
	// MERGE request is followed by a GET request within the same batch request.
	// BCP: 2070497030
	// JIRA: CPOUI5MODELS-379
	QUnit.test("ODataTreeBindingFlat: refreshAfterChange leads to GET", function (assert) {
		var oModel = createSpecialCasesModel({refreshAfterChange : true, useBatch : true}),
			sView = '\
<t:TreeTable id="table"\
		rows="{\
			parameters : {\
				countMode : \'Inline\',\
				gantt : {\
					rowIdName : \'OrderOperationRowID\'\
				},\
				treeAnnotationProperties : {\
					hierarchyDrillStateFor : \'OrderOperationIsExpanded\',\
					hierarchyLevelFor : \'OrderOperationRowLevel\',\
					hierarchyNodeDescendantCountFor : \'HierarchyDescendantCount\',\
					hierarchyNodeFor : \'OrderOperationRowID\',\
					hierarchyParentNodeFor : \'OrderOperationParentRowID\'\
				}\
			},\
			path : \'/C_RSHMaintSchedSmltdOrdAndOp\'\
		}"\
		visibleRowCount="1"\
		visibleRowCountMode="Fixed" \>\
	<Text id="maintenanceOrder" text="{MaintenanceOrder}" />\
</t:TreeTable>',
			that = this;

		this.expectHeadRequest()
			.expectRequest({
				batchNo : 1,
				deepPath : "/C_RSHMaintSchedSmltdOrdAndOp",
				requestUri : "C_RSHMaintSchedSmltdOrdAndOp?$skip=0&$top=101&$inlinecount=allpages"
					+ "&$filter=OrderOperationRowLevel%20le%200"
			}, {
				__count : "1",
				results : [{
					__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('~0~')"},
					MaintenanceOrder : "Foo"
				}]
			})
			.ignoreNullChanges("maintenanceOrder") //FIXME: unexpected change occurring in testsuite
			.expectChange("maintenanceOrder", ["Foo"]);

		return this.createView(assert, sView, oModel).then(function () {
			that.expectRequest({
					batchNo : 2,
					data : {
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('~0~')"},
						MaintenanceOrder : "Bar"
					},
					deepPath : "/C_RSHMaintSchedSmltdOrdAndOp('~0~')",
					headers : {},
					key : "C_RSHMaintSchedSmltdOrdAndOp('~0~')",
					method : "MERGE",
					requestUri : "C_RSHMaintSchedSmltdOrdAndOp('~0~')"
				}, NO_CONTENT)
				.expectRequest({
					batchNo : 2,
					deepPath : "/C_RSHMaintSchedSmltdOrdAndOp",
					requestUri : "C_RSHMaintSchedSmltdOrdAndOp?$skip=0&$top=101"
						+ "&$inlinecount=allpages&$filter=OrderOperationRowLevel%20le%200"
				}, {
					__count : "1",
					results : [{
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('~0~')"},
						MaintenanceOrder : "Bar"
					}]
				})
				.expectChange("maintenanceOrder", ["Bar"]);

			// code under test
			oModel.setProperty("/C_RSHMaintSchedSmltdOrdAndOp('~0~')/MaintenanceOrder", "Bar");
			oModel.submitChanges();

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Controls with PropertyBinding and CompositeBinding discard invalid values when the
	// context gets overwritten by another context that has the same values stored in the model.
	// JIRA: CPOUI5MODELS-336
[true, false].forEach(function (bUseStatic) {
	var sTitle = "CompositeBinding: Overwrite invalid entry with model value after context switch, "
			+ "static = " + bUseStatic;

	QUnit.test(sTitle, function (assert) {
		var oAmount0,
			oCurrency0,
			sCurrencyCodeJSON = "path : 'JSONModel>CurrencyCode',"
				+ "type : 'sap.ui.model.odata.type.String'",
			sCurrencyCodeStatic = "value : 'USD'",
			oJSONModel = new JSONModel({"CurrencyCode" : "USD"}),
			oModel = createSalesOrdersModel({
				defaultBindingMode : BindingMode.TwoWay,
				refreshAfterChange : false
			}),
			sView = '\
<FlexBox binding="{/SalesOrderSet(\'1\')}">\
	<Input id="Amount0" value="{\
			formatOptions : {showMeasure : false},\
			parts : [{\
				path : \'GrossAmount\',\
				type : \'sap.ui.model.odata.type.Decimal\'\
			}, {'
				+ (bUseStatic ? sCurrencyCodeStatic : sCurrencyCodeJSON) +
			'}],\
			type : \'sap.ui.model.type.Currency\'\
		}" />\
	<Input id="Currency0" value="{\
			constraints : {maxLength : 3},\
			path : \'CurrencyCode\',\
			type : \'sap.ui.model.odata.type.String\'\
		}" />\
</FlexBox>\
<FlexBox binding="{/SalesOrderSet(\'2\')}">\
	<Input id="Amount1" value="{GrossAmount}" />\
	<Input id="Currency1" value="{CurrencyCode}" />\
</FlexBox>',
			that = this;

		this.expectRequest("SalesOrderSet('1')", {
				CurrencyCode : "USD",
				GrossAmount : "10",
				SalesOrderID : "1"
			})
			.expectRequest("SalesOrderSet('2')", {
				CurrencyCode : "USD",
				GrossAmount : "10",
				SalesOrderID : "2"
			})
			.expectChange("Amount0", "10.00")
			.expectChange("Currency0", "")
			.expectChange("Currency0", "USD")
			.expectChange("Amount1", null)
			.expectChange("Amount1", "10")
			.expectChange("Currency1", null)
			.expectChange("Currency1", "USD");

		return this.createView(assert, sView, {undefined : oModel, JSONModel : oJSONModel}).then(
				function () {
			oAmount0 = that.oView.byId("Amount0");
			oCurrency0 = that.oView.byId("Currency0");
			// remove the formatter so that we can call setValue at the control
			oAmount0.getBinding("value").setFormatter(undefined);
			oCurrency0.getBinding("value").setFormatter(undefined);

			that.expectMessages([{
				descriptionUrl : undefined,
				message : "Enter a valid currency amount",
				target : oAmount0.getId() + "/value",
				type : "Error"
			}, {
				descriptionUrl : undefined,
				message : "Enter a text with a maximum of 3 characters and spaces",
				target : oCurrency0.getId() + "/value",
				type : "Error"
			}]);

			// code under test
			oAmount0.setValue("invalid amount");
			oCurrency0.setValue("invalid currency");

			return Promise.all([
				that.checkValueState(assert, oAmount0, "Error", "Enter a valid currency amount"),
				that.checkValueState(assert, oCurrency0, "Error",
					"Enter a text with a maximum of 3 characters and spaces"),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			that.expectMessages([]);

			// code under test
			oAmount0.setBindingContext(that.oView.byId("Amount1").getBindingContext());
			oCurrency0.setBindingContext(that.oView.byId("Currency1").getBindingContext());

			assert.strictEqual(oAmount0.getValue(), "10.00");
			assert.strictEqual(oAmount0.getBindingContext().getPath(), "/SalesOrderSet('2')");
			assert.strictEqual(oCurrency0.getValue(), "USD");
			assert.strictEqual(oCurrency0.getBindingContext().getPath(), "/SalesOrderSet('2')");

			return Promise.all([
				that.checkValueState(assert, oAmount0, "None", ""),
				that.checkValueState(assert, oCurrency0, "None", ""),
				that.waitForChanges(assert)
			]);
		});
	});
});

	//*********************************************************************************************
	// Scenario: One model instance is defined as two named models. Setting a new binding context to
	// a composite binding must only use the context that is related to the expected named model.
	// JIRA: CPOUI5MODELS-336
	QUnit.test("CompositeBinding: Set binding context; one model instance for two named models",
			function (assert) {
		var oModel = createSalesOrdersModel({useBatch : true}),
			sView = '\
<Table id="SalesOrderList" items="{/SalesOrderSet}">\
	<Text id="SalesOrderNote" text="{Note}" />\
</Table>\
<Table id="LineItems0" items="{ToLineItems}">\
	<Text id="LineItems0Note" text="{ItemPosition}: {Note}" />\
</Table>',
			that = this;

		this.expectHeadRequest()
			.expectRequest({
				deepPath : "/SalesOrderSet",
				requestUri : "SalesOrderSet?$skip=0&$top=100"
			}, {
				results : [{
					__metadata : {uri : "SalesOrderSet('1')"},
					Note : "Foo",
					SalesOrderID : "1"
				}]
			})
			.expectChange("LineItems0Note", false);

		return this.createView(assert, sView, {undefined : oModel, model2 : oModel}).then(
				function () {
			that.expectRequest({
					deepPath : "/SalesOrderSet('1')/ToLineItems",
					requestUri : "SalesOrderSet('1')/ToLineItems?$skip=0&$top=100"
				}, {
					results : [{
						__metadata : {
							uri : "SalesOrderLineItemSet(SalesOrderID='1',ItemPosition='10~0~')"
						},
						ItemPosition : "10~0~",
						Note : "note0",
						SalesOrderID : "1"
					}]
				})
				.expectChange("LineItems0Note", ["10~0~: note0"]);

			// code under test
			that.oView.byId("LineItems0").setBindingContext(
				that.oView.byId("SalesOrderList").getItems()[0].getBindingContext()
			);

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: Value state of a control gets updates when removing a binding of a property.
	// BCP: 2070436327
	QUnit.test("JSONModel: Value state updated after calling unbindProperty", function (assert) {
		var oModel = new JSONModel({Note : "Foo"}),
			sView = '<Input id="note" value="{/Note}" />',
			that = this;

		return this.createView(assert, sView, oModel).then(function () {
			assert.strictEqual(that.oView.byId("note").getValue("value"), "Foo");

			that.expectMessages([{
					descriptionUrl : undefined,
					message : "Some message",
					target : "/Note",
					type : "Error"
				}]);

			sap.ui.getCore().getMessageManager().addMessages(new Message({
				message: "Some message",
				processor: oModel,
				target: "/Note",
				type: "Error"
			}));

			return Promise.all([
				that.checkValueState(assert, "note", "Error", "Some message"),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			// code under test
			that.oView.byId("note").unbindProperty("value");

			return Promise.all([
				that.checkValueState(assert, "note", "None", ""),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			assert.strictEqual(that.oView.byId("note").getValue("value"), "");
		});
	});

	//*********************************************************************************************
	// Scenario: Value state of a control gets updates when setting another model.
	// BCP: 2070436327
	QUnit.test("JSONModel: Correct value state after setting another model", function (assert) {
		var oModel = new JSONModel({Note : "Foo"}),
			sView = '<Input id="note" value="{/Note}" />',
			that = this;

		return this.createView(assert, sView, oModel).then(function () {
			assert.strictEqual(that.oView.byId("note").getValue("value"), "Foo");

			that.expectMessages([{
					descriptionUrl : undefined,
					message : "Some message",
					target : "/Note",
					type : "Error"
				}]);

			sap.ui.getCore().getMessageManager().addMessages(new Message({
				message: "Some message",
				processor: oModel,
				target: "/Note",
				type: "Error"
			}));

			return Promise.all([
				that.checkValueState(assert, "note", "Error", "Some message"),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			// code under test
			that.oView.setModel(new JSONModel({Note : "Bar"}));

			return Promise.all([
				that.checkValueState(assert, "note", "None", ""),
				that.waitForChanges(assert)
			]);
		}).then(function () {
			assert.strictEqual(that.oView.byId("note").getValue("value"), "Bar");
		});
	});

	//*********************************************************************************************
	// Scenario: If the OData service has no customizing for units, the OData Unit type uses the
	// UI5 built-in CLDR information for formatting and parsing.
	// JIRA: CPOUI5MODELS-423
	QUnit.test("OData Unit type without unit customizing falls back to CLDR", function (assert) {
		var oModel = createSalesOrdersModel({defaultBindingMode : "TwoWay", useBatch : true}),
			sView = '\
<FlexBox binding="{/ProductSet(\'P1\')}">\
	<Input id="weight" value="{\
		parts : [{\
			constraints : { scale : \'variable\' },\
			path : \'WeightMeasure\',\
			type : \'sap.ui.model.odata.type.Decimal\'\
		}, {\
			path : \'WeightUnit\',\
			type : \'sap.ui.model.odata.type.String\'\
		}, {\
			mode : \'OneTime\',\
			path : \'/##@@requestUnitsOfMeasure\',\
			targetType : \'any\'\
		}],\
		mode : \'TwoWay\',\
		type : \'sap.ui.model.odata.type.Unit\'\
	}" />\
</FlexBox>',
			that = this;

		this.expectHeadRequest()
			.expectRequest("ProductSet('P1')", {
				ProductID : "P1",
				WeightMeasure : "12.34",
				WeightUnit : "mass-kilogram"
			})
			.expectChange("weight", "12.34")
			.expectChange("weight", "12.34 kg");

		return this.createView(assert, sView, oModel).then(function () {
			var oControl = that.oView.byId("weight");

			// remove the formatter so that we can call setValue at the control
			oControl.getBinding("value").setFormatter(null);

			// code under test
			oControl.setValue("23.4 kg");

			assert.strictEqual(oControl.getValue(), "23.4 kg");

			// code under test
			oControl.setValue("");

			assert.strictEqual(oControl.getValue(), "0 kg");
		});
	});

	//*********************************************************************************************
	// Scenario: If the OData service has no customizing for currencies, the OData Currency type
	// uses the UI5 built-in CLDR information for formatting and parsing.
	// JIRA: CPOUI5MODELS-423
	QUnit.test("OData Currency type without customizing falls back to CLDR", function (assert) {
		var oModel = createSalesOrdersModel({defaultBindingMode : "TwoWay", useBatch : true}),
			sView = '\
<FlexBox binding="{/ProductSet(\'P1\')}">\
	<Input id="price" value="{\
		parts : [{\
			constraints : { scale : \'variable\' },\
			path : \'Price\',\
			type : \'sap.ui.model.odata.type.Decimal\'\
		}, {\
			path : \'CurrencyCode\',\
			type : \'sap.ui.model.odata.type.String\'\
		}, {\
			mode : \'OneTime\',\
			path : \'/##@@requestCurrencyCodes\',\
			targetType : \'any\'\
		}],\
		mode : \'TwoWay\',\
		type : \'sap.ui.model.odata.type.Currency\'\
	}" />\
</FlexBox>',
			that = this;

		this.expectHeadRequest()
			.expectRequest("ProductSet('P1')", {
				ProductID : "P1",
				Price : "12.3",
				CurrencyCode : "EUR"
			})
			.expectChange("price", "12.30")
			.expectChange("price", "12.30\u00a0EUR"); // "\u00a0" is a non-breaking space

		return this.createView(assert, sView, oModel).then(function () {
			var oControl = that.oView.byId("price");

			// remove the formatter so that we can call setValue at the control
			oControl.getBinding("value").setFormatter(null);

			// code under test
			oControl.setValue("42 JPY");

			assert.strictEqual(oControl.getValue(), "42\u00a0JPY");

			// code under test
			oControl.setValue("");

			assert.strictEqual(oControl.getValue(), "0\u00a0JPY");
		});
	});

	//*********************************************************************************************
	// Scenario: TreeTable#collapseAll for a table using ODataTreeBindingAdapter resets the number
	// of levels expanded automatically in subsequent read requests to 0.
	// BCP: 66039 / 2021
	QUnit.test("ODataTreeBindingAdapter: collapseToLevel prevents auto expand of child nodes with"
			+ " higher level", function (assert) {
		var oModel = createSpecialCasesModel(),
			oTable,
			sView = '\
<t:TreeTable id="table"\
		rows="{\
			parameters : {\
				countMode : \'Inline\',\
				numberOfExpandedLevels : 1,\
				treeAnnotationProperties : {\
					hierarchyDrillStateFor : \'OrderOperationIsExpanded\',\
					hierarchyLevelFor : \'OrderOperationRowLevel\',\
					hierarchyNodeFor : \'OrderOperationRowID\',\
					hierarchyParentNodeFor : \'OrderOperationParentRowID\'\
				}\
			},\
			path : \'/C_RSHMaintSchedSmltdOrdAndOp\'\
		}"\
		threshold="0"\
		visibleRowCount="2"\
		visibleRowCountMode="Fixed" \>\
	<Text id="maintenanceOrder" text="{MaintenanceOrder}" />\
</t:TreeTable>',
			that = this;

		this.expectRequest("C_RSHMaintSchedSmltdOrdAndOp?$filter=OrderOperationRowLevel%20eq%200"
				+ "&$skip=0&$top=2&$inlinecount=allpages",
				{
					__count : "273",
					results : [{
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('id-0')"},
						MaintenanceOrder : "0",
						OrderOperationIsExpanded : "collapsed",
						OrderOperationRowID : "id-0",
						OrderOperationRowLevel : 0
					}, {
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('id-1')"},
						MaintenanceOrder : "1",
						OrderOperationIsExpanded : "leaf",
						OrderOperationRowID : "id-1",
						OrderOperationRowLevel : 0
					}]
				})
			.expectRequest("C_RSHMaintSchedSmltdOrdAndOp?"
				+ "$filter=OrderOperationParentRowID%20eq%20%27id-0%27&$skip=0&$top=2"
				+ "&$inlinecount=allpages",
				{
					__count : "5",
					results : [{
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('id-0.0')"},
						MaintenanceOrder : "0.0",
						OrderOperationIsExpanded : "leaf",
						OrderOperationParentRowID : "id-0",
						OrderOperationRowID : "id-0.0",
						OrderOperationRowLevel : 1
					}, {
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('id-0.1')"},
						MaintenanceOrder : "0.1",
						OrderOperationIsExpanded : "leaf",
						OrderOperationParentRowID : "id-0",
						OrderOperationRowID : "id-0.1",
						OrderOperationRowLevel : 1
					}]
				})
			.expectValue("maintenanceOrder", ["0", "1"])
			.expectValue("maintenanceOrder", "0.0", 1);

		return this.createView(assert, sView, oModel).then(function () {
			oTable = that.oView.byId("table");

			that.expectValue("maintenanceOrder", "1", 1);

			// code under test
			oTable.collapseAll();

			return that.waitForChanges(assert);
		}).then(function () {
			//TODO expect $top=2 instead of $top=4, check TreeBindingAdapter#_getContextsOrNodes?
			that.expectRequest("C_RSHMaintSchedSmltdOrdAndOp"
				+ "?$filter=OrderOperationRowLevel%20eq%200&$skip=2&$top=4",
				{
					results : [{
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('id-2')"},
						MaintenanceOrder : "2",
						OrderOperationIsExpanded : "collapsed",
						OrderOperationRowID : "id-2",
						OrderOperationRowLevel : 0
					}, {
						__metadata : {uri : "C_RSHMaintSchedSmltdOrdAndOp('id-3')"},
						MaintenanceOrder : "3",
						OrderOperationIsExpanded : "leaf",
						OrderOperationRowID : "id-3",
						OrderOperationRowLevel : 0
					}]
				})
				.expectValue("maintenanceOrder", "", 2)
				.expectValue("maintenanceOrder", "", 3)
				.expectValue("maintenanceOrder", "2", 2)
				.expectValue("maintenanceOrder", "3", 3);

			// code under test
			// scroll down shows additional level 0 nodes, but must NOT load or show their children
			oTable.setFirstVisibleRow(2);

			return that.waitForChanges(assert);
		});
	});

	//*********************************************************************************************
	// Scenario: If the OData service works with code list for units, the OData Unit type uses this
	// information for formatting and parsing.
	// JIRA: CPOUI5MODELS-437
	QUnit.test("OData Unit type with code list for units", function (assert) {
		var oModel = createModel("/sap/opu/odata/sap/ZUI5_GWSAMPLE_BASIC?foo=bar", {
				defaultBindingMode : "TwoWay",
				metadataUrlParams : {"customMeta" : "custom/meta"},
				serviceUrlParams : {"customService" : "custom/service"},
				tokenHandling : false,
				useBatch : true
			}),
			sView = '\
<FlexBox binding="{/ProductSet(\'P1\')}">\
	<Input id="weight" value="{\
		parts : [{\
			constraints : { scale : \'variable\' },\
			path : \'WeightMeasure\',\
			type : \'sap.ui.model.odata.type.Decimal\'\
		}, {\
			path : \'WeightUnit\',\
			type : \'sap.ui.model.odata.type.String\'\
		}, {\
			mode : \'OneTime\',\
			path : \'/##@@requestUnitsOfMeasure\',\
			targetType : \'any\'\
		}],\
		mode : \'TwoWay\',\
		type : \'sap.ui.model.odata.type.Unit\'\
	}" />\
</FlexBox>',
			that = this;

		this.expectRequest("ProductSet('P1')?foo=bar&customService=custom%2Fservice", {
				ProductID : "P1",
				WeightMeasure : "12.34",
				WeightUnit : "KG"
			})
			.expectRequest(
				"SAP__UnitsOfMeasure?foo=bar&customService=custom%2Fservice&$skip=0&$top=5000", {
				results : [{
					DecimalPlaces : 3,
					ExternalCode : "KG",
					ISOCode : "KGM",
					Text : "Kilogramm",
					UnitCode : "KG"
				}]
			})
			.expectChange("weight", "12.340 KG");

		return this.createView(assert, sView, oModel).then(function () {
			var oControl = that.oView.byId("weight");

			// remove the formatter so that we can call setValue at the control
			oControl.getBinding("value").setFormatter(null);

			// code under test
			oControl.setValue("23.4 KG");

			assert.strictEqual(oControl.getValue(), "23.400 KG");

			// code under test
			oControl.setValue("");

			assert.strictEqual(oControl.getValue(), "0.000 KG");

			that.expectMessages([{
				descriptionUrl: undefined,
				message : "EnterNumberFraction 3",
				target : oControl.getId() + "/value",
				type : "Error"
			}]);

			TestUtils.withNormalizedMessages(function () {
				// code under test
				oControl.setValue("12.3456 KG");
			});

			return that.waitForChanges(assert);
		}).then(function () {
			return that.checkValueState(assert, that.oView.byId("weight"), "Error",
				"EnterNumberFraction 3");
		});
	});

	//*********************************************************************************************
	// Scenario: If the OData service works with code list for currencies, the OData Currency type
	// uses this information for formatting and parsing.
	// JIRA: CPOUI5MODELS-437
	QUnit.test("OData Currency type with code list for currencies", function (assert) {
		var oModel = createModel("/sap/opu/odata/sap/ZUI5_GWSAMPLE_BASIC?foo=bar", {
				defaultBindingMode : "TwoWay",
				metadataUrlParams : {"customMeta" : "custom/meta"},
				serviceUrlParams : {"customService" : "custom/service"},
				tokenHandling : false,
				useBatch : true
			}),
			sView = '\
<FlexBox binding="{/ProductSet(\'P1\')}">\
	<Input id="price" value="{\
		parts : [{\
			constraints : { scale : \'variable\' },\
			path : \'Price\',\
			type : \'sap.ui.model.odata.type.Decimal\'\
		}, {\
			path : \'CurrencyCode\',\
			type : \'sap.ui.model.odata.type.String\'\
		}, {\
			mode : \'OneTime\',\
			path : \'/##@@requestCurrencyCodes\',\
			targetType : \'any\'\
		}],\
		mode : \'TwoWay\',\
		type : \'sap.ui.model.odata.type.Currency\'\
	}" />\
</FlexBox>',
			that = this;

		this.expectRequest("ProductSet('P1')?foo=bar&customService=custom%2Fservice", {
				ProductID : "P1",
				Price : "12.3",
				CurrencyCode : "EUR"
			})
			.expectRequest(
				"SAP__Currencies?foo=bar&customService=custom%2Fservice&$skip=0&$top=5000", {
				results : [{
					CurrencyCode : "EUR",
					DecimalPlaces : 2,
					ISOCode : "EUR",
					Text : "Euro"
				}, {
					CurrencyCode : "USDN",
					DecimalPlaces : 5,
					ISOCode : "",
					Text : "US Dollar"
				}]
			})
			// "\u00a0" is a non-breaking space
			.expectChange("price", "12.30\u00a0EUR");

		return this.createView(assert, sView, oModel).then(function () {
			var oControl = that.oView.byId("price");

			// remove the formatter so that we can call setValue at the control
			oControl.getBinding("value").setFormatter(null);

			// code under test
			oControl.setValue("42.12345 USDN");

			assert.strictEqual(oControl.getValue(), "42.12345\u00a0USDN");

			// code under test
			oControl.setValue("");

			assert.strictEqual(oControl.getValue(), "0.00000\u00a0USDN");

			that.expectMessages([{
				descriptionUrl: undefined,
				message : "EnterNumberFraction 2",
				target : oControl.getId() + "/value",
				type : "Error"
			}]);

			TestUtils.withNormalizedMessages(function () {
				// code under test
				oControl.setValue("1.234 EUR");
			});

			return that.waitForChanges(assert);
		}).then(function () {
			return that.checkValueState(assert, that.oView.byId("price"), "Error",
				"EnterNumberFraction 2");
		});
	});
});