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 @@
* 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.
* <tr>
......@@ -9,30 +59,21 @@
* <td>field_my_file</td>
* <td>/data.jpg myfilename.jpg</td>
* </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) {
var tmpArray = url_and_filename.split(' ', 2);
var url = tmpArray[0];
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) {
throw new Error('file name must not be empty.');
}
var fileField = this.page().findElement(locator);
if (!fileName) {
throw new Error('file name must not be empty.');
}
var fileField = this.page().findElement(locator);
return wrapPromise(
fetch(url)
.then(function(response) {
if (!response.ok) {
......@@ -48,19 +89,7 @@ Selenium.prototype.doSetFile = function(locator, url_and_filename) {
new DataTransfer();
dT.items.add(new File([blob], fileName));
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) {
* Helper function to generate a DOM elements
*
* @param {string} tagName name of the element
* @param {Node?} childList list of child elements
* @param {Map<string,any>?} attributeDict attributes
* @param {string?} textContent
* @return {Node}
* @param {Node[]} [childList] list of child elements
* @param {Object} [attributeDict] attributes
* @param {string} [textContent]
* @return {HTMLElement}
*/
function generateElement(tagName, childList, attributeDict, textContent) {
var element = document.createElement(tagName);
......@@ -174,6 +203,9 @@ function generateElement(tagName, childList, attributeDict, textContent) {
element.appendChild(child);
});
}
if (textContent) {
element.textContent = textContent;
}
return element;
}
......@@ -248,49 +280,34 @@ function generateUpdateForm(referenceImageURL, newImageData) {
/**
* verify that the rendering of the element `locator` matches the previously saved reference.
*
* Arguments:
* locator - an element locator
* misMatchTolerance - the percentage of mismatch allowed. If this is 0, the
* Note that this is implemented as do* method and not a assert* method because only do* methods are asynchronous.
*
* @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.
* @returns {() => boolean}
*/
Selenium.prototype.doVerifyImageMatchSnapshot = (
locator,
misMatchTolerance
) => {
// XXX this is a do* method and not a assert* method because only do* methods are
// asynchronous.
// 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
var promiseState, rejectionValue, canvasPromise;
return function assertCanvasImage() {
if (promiseState === 'pending') {
return false;
}
if (promiseState === 'resolved') {
return true;
}
if (promiseState === 'rejected') {
Assert.fail(rejectionValue);
}
var misMatchToleranceFloat = parseFloat(misMatchTolerance);
if (isNaN(misMatchToleranceFloat)) {
misMatchToleranceFloat = 0;
}
misMatchTolerance = parseFloat(misMatchTolerance);
if (isNaN(misMatchTolerance)) {
misMatchTolerance = 0;
}
promiseState = 'pending';
element = selenium.browserbot.findElement(locator);
if (element.nodeName == 'CANVAS' /* instanceof HTMLCanvasElement XXX ? */) {
canvasPromise = Promise.resolve(element);
} else {
canvasPromise = html2canvas(element);
}
/** @type {Promise<HTMLCanvasElement>} */
var canvasPromise;
var element = selenium.browserbot.findElement(locator);
if (element.nodeName == 'CANVAS' /* instanceof HTMLCanvasElement XXX ? */) {
canvasPromise = Promise.resolve(element);
} else {
canvasPromise = html2canvas(element);
}
return wrapPromise(
canvasPromise
.then(canvas => {
return canvas.toDataURL();
......@@ -310,12 +327,12 @@ Selenium.prototype.doVerifyImageMatchSnapshot = (
blob => {
return new Promise((resolve, reject) => {
var fr = new FileReader();
fr.onload = d => resolve(fr.result);
fr.onload = () => resolve(fr.result);
fr.onerror = reject;
fr.readAsDataURL(blob);
});
},
e => {
() => {
// fetching reference was not found, return empty image instead, it will be different
// (unless the tolerance is too high)
return document.createElement('canvas').toDataURL();
......@@ -328,16 +345,14 @@ Selenium.prototype.doVerifyImageMatchSnapshot = (
useCrossOrigin: false
})
.compareTo(expected);
if (misMatchTolerance > 0) {
if (misMatchToleranceFloat > 0) {
comparator = comparator.scaleToSameSize();
}
comparator.onComplete(resolve);
});
})
.then(diff => {
if (diff.rawMisMatchPercentage <= misMatchTolerance) {
promiseState = 'resolved';
} else {
if (diff.rawMisMatchPercentage > misMatchToleranceFloat) {
return generateUpdateForm(referenceImageURL, actual).then(
updateReferenceImageForm => {
htmlTestRunner.currentTest.currentRow.trElement
......@@ -364,19 +379,11 @@ Selenium.prototype.doVerifyImageMatchSnapshot = (
)
])
);
promiseState = 'rejected';
rejectionValue =
'Images are ' + diff.misMatchPercentage + '% different';
throw new Error('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