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

ui_test_core: new verifyImageMatchSnapshot

To Compare a "screenshot" of a DOM element with a reference snapshot.

This check supports the following parameters:
 *   locator - an element locator
 *   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.
parent 818ae8f9
from StringIO import StringIO
portal = context.getPortalObject()
image_file = StringIO(image_data.replace('data:image/png;base64,', '').decode('base64'))
image_path = image_path.split('/')
existing = portal.restrictedTraverse(image_path, None)
if existing is None:
container = portal.restrictedTraverse(image_path[:-1])
container.manage_addProduct['OFSP'].manage_addImage(
image_path[-1],
image_file,
'')
else:
existing.manage_upload(image_file)
return "reference image at {} updated".format('/'.join(image_path))
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>image_data, image_path</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Zuite_updateReferenceImage</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -82,3 +82,251 @@ Selenium.prototype.assertElementPositionRangeTop = function(locator, range){ ...@@ -82,3 +82,251 @@ Selenium.prototype.assertElementPositionRangeTop = function(locator, range){
Assert.fail(positionTop + " is not between " + minimumPositionTop + " and " + maximumPositionTop); Assert.fail(positionTop + " is not between " + minimumPositionTop + " and " + maximumPositionTop);
} }
}; };
// a memo test pathname => image counter
// TODO: reset this on testSuite.reset(), because we cannot re-run a test.
imageMatchReference = new Map();
function getReferenceImageCounter(testPathName) {
var counter = imageMatchReference.get(testPathName);
if (counter !== undefined) {
return counter;
}
counter = imageMatchReference.size + 1;
imageMatchReference.set(testPathName, counter);
return counter;
}
function getReferenceImageURL(testPathName) {
var imageCounter = getReferenceImageCounter(testPathName);
return testPathName + '-reference-snapshot-' + imageCounter + '.png';
}
/**
*
* 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}
*/
function generateElement(tagName, childList, attributeDict, textContent) {
var element = document.createElement(tagName);
if (attributeDict) {
for (var attr in attributeDict) {
element.setAttribute(attr, attributeDict[attr]);
}
}
if (childList) {
childList.map(child => {
element.appendChild(child);
});
}
return element;
}
/**
* Generate an HTML form to update the reference snapshot
*
* @param {string} referenceImageURL relative URL of the reference image
* @param {string} newImageData the new image data, base64 encoded
* @param {Map<string,any>?} attributeDict attributes
* @return {Promise<string>} the base64 encoded html form
*/
function generateUpdateForm(referenceImageURL, newImageData) {
return new Promise((resolve, reject) => {
var fr = new FileReader();
fr.onerror = reject;
fr.onload = () => resolve(fr.result);
fr.readAsDataURL(
new Blob(
[
generateElement('html', [
generateElement('body', [
generateElement('p', [
document.createTextNode('Replacing this old snapshot:'),
generateElement('br'),
generateElement('img', [], {
src: location.origin + referenceImageURL,
alt: 'reference image'
}),
generateElement('br'),
document.createTextNode('with this new snapshot:'),
generateElement('br'),
generateElement('img', [], {
src: newImageData,
alt: 'new image'
})
]),
generateElement(
'form',
[
generateElement('input', [], {
type: 'hidden',
name: 'image_data',
value: newImageData
}),
generateElement('input', [], {
type: 'hidden',
name: 'image_path',
value: referenceImageURL
}),
generateElement('input', [], {
type: 'submit',
value: 'Update Reference Snapshot'
})
],
{
action:
location.origin +
'/' +
referenceImageURL.split('/')[1] + // ERP5 portal
'/Zuite_updateReferenceImage',
method: 'POST'
}
)
])
]).innerHTML
],
{ type: 'text/html' }
)
);
});
}
/**
* 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
* images must be exactly same. If more than 0, image will also be resized.
*/
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);
}
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);
}
canvasPromise
.then(canvas => {
return canvas.toDataURL();
})
.then(actual => {
var referenceImageURL = getReferenceImageURL(
testFrame.getCurrentTestCase().pathname
);
return fetch(referenceImageURL)
.then(response => {
if (response.status === 200) {
return response.blob();
}
throw new Error('Feching reference failed ' + response.statusText);
})
.then(
blob => {
return new Promise((resolve, reject) => {
var fr = new FileReader();
fr.onload = d => resolve(fr.result);
fr.onerror = reject;
fr.readAsDataURL(blob);
});
},
e => {
// fetching reference was not found, return empty image instead, it will be different
return document.createElement('canvas').toDataURL();
}
)
.then(expected => {
return new Promise(resolve => {
var comparator = resemble(actual)
.outputSettings({
useCrossOrigin: false
})
.compareTo(expected);
if (misMatchTolerance > 0) {
comparator = comparator.scaleToSameSize();
}
comparator.onComplete(resolve);
});
})
.then(diff => {
if (diff.rawMisMatchPercentage <= misMatchTolerance) {
promiseState = 'resolved';
} else {
return generateUpdateForm(referenceImageURL, actual).then(
updateReferenceImageForm => {
htmlTestRunner.currentTest.currentRow.trElement
.querySelector('td')
.appendChild(
generateElement('div', [
document.createTextNode('Image differences:'),
generateElement('br'),
generateElement('img', [], {
src: diff.getImageDataUrl(),
alt: 'Image differences'
}),
generateElement('br'),
document.createTextNode('Click '),
generateElement(
'a',
[document.createTextNode('here')],
{
href: updateReferenceImageForm
}
),
document.createTextNode(
' to update reference snapshot.'
)
])
);
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