Commit f596613d authored by Jérome Perrin's avatar Jérome Perrin

Error handling and css fonts in Selenium extensions

See merge request nexedi/erp5!1220
parents b683393f 3ef27120
Pipeline #10813 failed with stage
...@@ -2,6 +2,56 @@ ...@@ -2,6 +2,56 @@
* Selenium extensions for the ERP5 project * Selenium extensions for the ERP5 project
*/ */
/**
* Wrap a promise to make it usable by selenium commands.
*
* If the promise is rejected, the command will fail with the promise rejection value.
* If the promise is resovled, the resolved value is not used.
*
* The asynchronicity of do* method is as follow Selenium.prototype.doXXX
* returns a function and this function will be called again and again until:
* * function returns true, which means step is successfull
* * function returns false, which means step is not finished and function will be called again
* * an execption is raised, in that case the step is failed
* * global timeout is reached.
* we implement the state management with similar approach as what's discussed
* https://stackoverflow.com/questions/30564053/how-can-i-synchronously-determine-a-javascript-promises-state
*
* @param {Promise} promise the promise to await
* @returns {() => boolean}
*/
function wrapPromise(promise) {
/** @type {'pending' | 'resolved' | 'rejected'} */
var promiseState;
var rejectionValue;
return () => {
if (promiseState === 'pending') {
return false;
}
if (promiseState === 'resolved') {
return true;
}
if (promiseState === 'rejected') {
Assert.fail("" + rejectionValue);
return true
}
promise.then(
function () {
promiseState = 'resolved';
}).catch(
function (error) {
console.error(error);
promiseState = 'rejected';
rejectionValue = error;
}
);
promiseState = 'pending';
return false;
}
}
/** /**
* You can set file data to file input field without security error. * You can set file data to file input field without security error.
* <tr> * <tr>
...@@ -9,30 +59,21 @@ ...@@ -9,30 +59,21 @@
* <td>field_my_file</td> * <td>field_my_file</td>
* <td>/data.jpg myfilename.jpg</td> * <td>/data.jpg myfilename.jpg</td>
* </tr> * </tr>
*
* @param {string} locator the selenium locator
* @param {string} url_and_filename the URL and filename, separated by space
* @returns {() => boolean}
*/ */
Selenium.prototype.doSetFile = function(locator, url_and_filename) { Selenium.prototype.doSetFile = function(locator, url_and_filename) {
var tmpArray = url_and_filename.split(' ', 2); var tmpArray = url_and_filename.split(' ', 2);
var url = tmpArray[0]; var url = tmpArray[0];
var fileName = tmpArray[1]; var fileName = tmpArray[1];
var rejectionValue,
promiseState;
// same technique as doVerifyImageMatchSnapshot below
var assertFileSet = () => {
if (promiseState === 'pending') {
return false;
}
if (promiseState === 'resolved') {
return true;
}
if (promiseState === 'rejected') {
Assert.fail(rejectionValue);
}
promiseState = 'pending';
if (!fileName) { if (!fileName) {
throw new Error('file name must not be empty.'); throw new Error('file name must not be empty.');
} }
var fileField = this.page().findElement(locator); var fileField = this.page().findElement(locator);
return wrapPromise(
fetch(url) fetch(url)
.then(function(response) { .then(function(response) {
if (!response.ok) { if (!response.ok) {
...@@ -48,19 +89,7 @@ Selenium.prototype.doSetFile = function(locator, url_and_filename) { ...@@ -48,19 +89,7 @@ Selenium.prototype.doSetFile = function(locator, url_and_filename) {
new DataTransfer(); new DataTransfer();
dT.items.add(new File([blob], fileName)); dT.items.add(new File([blob], fileName));
fileField.files = dT.files; fileField.files = dT.files;
}) }));
.then(
function() {
promiseState = 'resolved';
},
function(error) {
console.error(error);
promiseState = 'rejected';
rejectionValue = 'Error setting file ' + error;
}
);
}
return assertFileSet;
}; };
...@@ -157,10 +186,10 @@ function getReferenceImageURL(testPathName) { ...@@ -157,10 +186,10 @@ function getReferenceImageURL(testPathName) {
* Helper function to generate a DOM elements * Helper function to generate a DOM elements
* *
* @param {string} tagName name of the element * @param {string} tagName name of the element
* @param {Node?} childList list of child elements * @param {Node[]} [childList] list of child elements
* @param {Map<string,any>?} attributeDict attributes * @param {Object} [attributeDict] attributes
* @param {string?} textContent * @param {string} [textContent]
* @return {Node} * @return {HTMLElement}
*/ */
function generateElement(tagName, childList, attributeDict, textContent) { function generateElement(tagName, childList, attributeDict, textContent) {
var element = document.createElement(tagName); var element = document.createElement(tagName);
...@@ -174,6 +203,9 @@ function generateElement(tagName, childList, attributeDict, textContent) { ...@@ -174,6 +203,9 @@ function generateElement(tagName, childList, attributeDict, textContent) {
element.appendChild(child); element.appendChild(child);
}); });
} }
if (textContent) {
element.textContent = textContent;
}
return element; return element;
} }
...@@ -248,49 +280,41 @@ function generateUpdateForm(referenceImageURL, newImageData) { ...@@ -248,49 +280,41 @@ function generateUpdateForm(referenceImageURL, newImageData) {
/** /**
* verify that the rendering of the element `locator` matches the previously saved reference. * verify that the rendering of the element `locator` matches the previously saved reference.
* *
* Arguments: * Note that this is implemented as do* method and not a assert* method because only do* methods are asynchronous.
* locator - an element locator *
* misMatchTolerance - the percentage of mismatch allowed. If this is 0, the * @param {string} locator - an element locator
* @param {string} misMatchTolerance - the percentage of mismatch allowed. If this is 0, the
* images must be exactly same. If more than 0, image will also be resized. * images must be exactly same. If more than 0, image will also be resized.
* @returns {() => boolean}
*/ */
Selenium.prototype.doVerifyImageMatchSnapshot = ( Selenium.prototype.doVerifyImageMatchSnapshot = (
locator, locator,
misMatchTolerance misMatchTolerance
) => { ) => {
// XXX this is a do* method and not a assert* method because only do* methods are var misMatchToleranceFloat = parseFloat(misMatchTolerance);
// asynchronous. if (isNaN(misMatchToleranceFloat)) {
// The asynchronicity of do* method is as follow Selenium.prototype.doXXX misMatchToleranceFloat = 0;
// returns a function and this function will be called again and again until: }
// * function returns true, which means step is successfull
// * function returns false, which means step is not finished and function will be called again
// * an execption is raised, in that case the step is failed
// * global timeout is reached.
// we implement the state management with similar approach as what's discussed
// https://stackoverflow.com/questions/30564053/how-can-i-synchronously-determine-a-javascript-promises-state
var promiseState, rejectionValue, canvasPromise;
return function assertCanvasImage() {
if (promiseState === 'pending') {
return false;
}
if (promiseState === 'resolved') {
return true;
}
if (promiseState === 'rejected') {
Assert.fail(rejectionValue);
}
misMatchTolerance = parseFloat(misMatchTolerance); /** @type {Promise<HTMLCanvasElement>} */
if (isNaN(misMatchTolerance)) { var canvasPromise;
misMatchTolerance = 0;
} /** @type {HTMLElement} */
promiseState = 'pending'; var element = selenium.browserbot.findElement(locator);
element = selenium.browserbot.findElement(locator);
if (element.nodeName == 'CANVAS' /* instanceof HTMLCanvasElement XXX ? */) { if (element.nodeName == 'CANVAS' /* instanceof HTMLCanvasElement XXX ? */) {
canvasPromise = Promise.resolve(element); canvasPromise = Promise.resolve(element);
} else { } else {
canvasPromise = html2canvas(element); // create a canvas in the same document, so that if this document has loaded
} // extra fonts they are also available.
// As suggested on https://github.com/niklasvh/html2canvas/issues/1772
var destinationCanvas = element.ownerDocument.createElement("canvas");
destinationCanvas.width = element.scrollWidth;
destinationCanvas.height = element.scrollHeight;
canvasPromise = html2canvas(element, { canvas: destinationCanvas });
}
return wrapPromise(
canvasPromise canvasPromise
.then(canvas => { .then(canvas => {
return canvas.toDataURL(); return canvas.toDataURL();
...@@ -310,12 +334,12 @@ Selenium.prototype.doVerifyImageMatchSnapshot = ( ...@@ -310,12 +334,12 @@ Selenium.prototype.doVerifyImageMatchSnapshot = (
blob => { blob => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var fr = new FileReader(); var fr = new FileReader();
fr.onload = d => resolve(fr.result); fr.onload = () => resolve(fr.result);
fr.onerror = reject; fr.onerror = reject;
fr.readAsDataURL(blob); fr.readAsDataURL(blob);
}); });
}, },
e => { () => {
// fetching reference was not found, return empty image instead, it will be different // fetching reference was not found, return empty image instead, it will be different
// (unless the tolerance is too high) // (unless the tolerance is too high)
return document.createElement('canvas').toDataURL(); return document.createElement('canvas').toDataURL();
...@@ -328,16 +352,14 @@ Selenium.prototype.doVerifyImageMatchSnapshot = ( ...@@ -328,16 +352,14 @@ Selenium.prototype.doVerifyImageMatchSnapshot = (
useCrossOrigin: false useCrossOrigin: false
}) })
.compareTo(expected); .compareTo(expected);
if (misMatchTolerance > 0) { if (misMatchToleranceFloat > 0) {
comparator = comparator.scaleToSameSize(); comparator = comparator.scaleToSameSize();
} }
comparator.onComplete(resolve); comparator.onComplete(resolve);
}); });
}) })
.then(diff => { .then(diff => {
if (diff.rawMisMatchPercentage <= misMatchTolerance) { if (diff.rawMisMatchPercentage > misMatchToleranceFloat) {
promiseState = 'resolved';
} else {
return generateUpdateForm(referenceImageURL, actual).then( return generateUpdateForm(referenceImageURL, actual).then(
updateReferenceImageForm => { updateReferenceImageForm => {
htmlTestRunner.currentTest.currentRow.trElement htmlTestRunner.currentTest.currentRow.trElement
...@@ -364,19 +386,11 @@ Selenium.prototype.doVerifyImageMatchSnapshot = ( ...@@ -364,19 +386,11 @@ Selenium.prototype.doVerifyImageMatchSnapshot = (
) )
]) ])
); );
throw new Error('Images are ' + diff.misMatchPercentage + '% different');
promiseState = 'rejected';
rejectionValue =
'Images are ' + diff.misMatchPercentage + '% different';
} }
); );
} }
}); });
}) }));
.catch(error => {
console.error(error);
promiseState = 'rejected';
rejectionValue = 'Error computing image differences ' + error;
});
};
}; };
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment