Commit 949f4a95 authored by Jérome Perrin's avatar Jérome Perrin

ui_test_core: utility function for Promise compatibility

Selenium asynchronicity does not use Promise, but calls function while
it returns true, we were implementing this logic in two places and
implementing wrongly because some case of errors where not reported.

Introduce a wrapper that makes turns Promise into a function returning a
boolean like selenium is using and use it in setFile and
verifyImageMatchSnapshot commands

Also a bit more of jsdoc comments for typescript.
parent 1fb5f27a
...@@ -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,34 @@ function generateUpdateForm(referenceImageURL, newImageData) { ...@@ -248,49 +280,34 @@ 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;
} var element = selenium.browserbot.findElement(locator);
promiseState = 'pending';
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); canvasPromise = html2canvas(element);
} }
return wrapPromise(
canvasPromise canvasPromise
.then(canvas => { .then(canvas => {
return canvas.toDataURL(); return canvas.toDataURL();
...@@ -310,12 +327,12 @@ Selenium.prototype.doVerifyImageMatchSnapshot = ( ...@@ -310,12 +327,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 +345,14 @@ Selenium.prototype.doVerifyImageMatchSnapshot = ( ...@@ -328,16 +345,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 +379,11 @@ Selenium.prototype.doVerifyImageMatchSnapshot = ( ...@@ -364,19 +379,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;
});
};
}; };
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