// This file expects imgutils.js to be loaded as well. /* import-globals-from imgutils.js */ /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ var currentTest; var gIsRefImageLoaded = false; const gShouldOutputDebugInfo = false; function pollForSuccess() { if (!currentTest.isTestFinished) { if ( !currentTest.reusingReferenceImage || (currentTest.reusingReferenceImage && gIsRefImageLoaded) ) { currentTest.checkImage(); } setTimeout(pollForSuccess, currentTest.pollFreq); } } function reuseImageCallback() { gIsRefImageLoaded = true; } function failTest() { if (currentTest.isTestFinished || currentTest.closeFunc) { return; } ok( false, "timing out after " + currentTest.timeout + "ms. " + "Animated image still doesn't look correct, after poll #" + currentTest.pollCounter ); currentTest.wereFailures = true; if (currentTest.currentSnapshotDataURI) { currentTest.outputDebugInfo( "Snapshot #" + currentTest.pollCounter, "snapNum" + currentTest.pollCounter, currentTest.currentSnapshotDataURI ); } currentTest.enableDisplay( document.getElementById(currentTest.debugElementId) ); currentTest.cleanUpAndFinish(); } /** * Create a new AnimationTest object. * * @param pollFreq The amount of time (in ms) to wait between consecutive * snapshots if the reference image and the test image don't match. * @param timeout The total amount of time (in ms) to wait before declaring the * test as failed. * @param referenceElementId The id attribute of the reference image element, or * the source of the image to change to, once the reference snapshot has * been successfully taken. This latter option could be used if you don't * want the image to become invisible at any time during the test. * @param imageElementId The id attribute of the test image element. * @param debugElementId The id attribute of the div where links should be * appended if the test fails. * @param cleanId The id attribute of the div or element to use as the 'clean' * test. This element is only enabled when we are testing to verify that * the reference image has been loaded. It can be undefined. * @param srcAttr The location of the source of the image, for preloading. This * is usually not required, but it useful for preloading reference * images. * @param xulTest A boolean value indicating whether or not this is a XUL test * (uses hidden=true/false rather than display: none to hide/show * elements). * @param closeFunc A function that should be called when this test is finished. * If null, then cleanUpAndFinish() will be called. This can be used to * chain tests together, so they are all finished exactly once. * @returns {AnimationTest} */ function AnimationTest( pollFreq, timeout, referenceElementId, imageElementId, debugElementId, cleanId, srcAttr, xulTest, closeFunc ) { // We want to test the cold loading behavior, so clear cache in case an // earlier test got our image in there already. clearAllImageCaches(); this.wereFailures = false; this.pollFreq = pollFreq; this.timeout = timeout; this.imageElementId = imageElementId; this.referenceElementId = referenceElementId; if (!document.getElementById(referenceElementId)) { // In this case, we're assuming the user passed in a string that // indicates the source of the image they want to change to, // after the reference image has been taken. this.reusingImageAsReference = true; } this.srcAttr = srcAttr; this.debugElementId = debugElementId; this.referenceSnapshot = ""; // value will be set in takeReferenceSnapshot() this.pollCounter = 0; this.isTestFinished = false; this.numRefsTaken = 0; this.blankWaitTime = 0; this.cleanId = cleanId ? cleanId : ""; this.xulTest = xulTest ? xulTest : ""; this.closeFunc = closeFunc ? closeFunc : ""; } AnimationTest.prototype.preloadImage = function() { if (this.srcAttr) { this.myImage = new Image(); this.myImage.onload = function() { currentTest.continueTest(); }; this.myImage.src = this.srcAttr; } else { this.continueTest(); } }; AnimationTest.prototype.outputDebugInfo = function(message, id, dataUri) { if (!gShouldOutputDebugInfo) { return; } var debugElement = document.getElementById(this.debugElementId); var newDataUriElement = document.createElement("a"); newDataUriElement.setAttribute("id", id); newDataUriElement.setAttribute("href", dataUri); newDataUriElement.appendChild(document.createTextNode(message)); debugElement.appendChild(newDataUriElement); var brElement = document.createElement("br"); debugElement.appendChild(brElement); todo(false, "Debug (" + id + "): " + message + " " + dataUri); }; AnimationTest.prototype.isFinished = function() { return this.isTestFinished; }; AnimationTest.prototype.takeCleanSnapshot = function() { var cleanElement; if (this.cleanId) { cleanElement = document.getElementById(this.cleanId); } // Enable clean page comparison element if (cleanElement) { this.enableDisplay(cleanElement); } // Take a snapshot of the initial (clean) page this.cleanSnapshot = snapshotWindow(window, false); // Disable the clean page comparison element if (cleanElement) { this.disableDisplay(cleanElement); } var dataString1 = "Clean Snapshot"; this.outputDebugInfo( dataString1, "cleanSnap", this.cleanSnapshot.toDataURL() ); }; AnimationTest.prototype.takeBlankSnapshot = function() { // Take a snapshot of the initial (essentially blank) page this.blankSnapshot = snapshotWindow(window, false); var dataString1 = "Initial Blank Snapshot"; this.outputDebugInfo( dataString1, "blank1Snap", this.blankSnapshot.toDataURL() ); }; /** * Begin the AnimationTest. This will utilize the information provided in the * constructor to invoke a mochitest on animated images. It will automatically * fail if allowed to run past the timeout. This will attempt to preload an * image, if applicable, and then asynchronously call continueTest(), or if not * applicable, synchronously trigger a call to continueTest(). */ AnimationTest.prototype.beginTest = function() { SimpleTest.waitForExplicitFinish(); SimpleTest.requestFlakyTimeout("untriaged"); currentTest = this; this.preloadImage(); }; /** * This is the second part of the test. It is triggered (eventually) from * beginTest() either synchronously or asynchronously, as an image load * callback. */ AnimationTest.prototype.continueTest = async function() { // In case something goes wrong, fail earlier than mochitest timeout, // and with more information. setTimeout(failTest, this.timeout); if (!this.reusingImageAsReference) { this.disableDisplay(document.getElementById(this.imageElementId)); } let tookReference = new Promise(resolve => { this.takeReferenceSnapshot(resolve); }); tookReference.then(() => { this.setupPolledImage(); SimpleTest.executeSoon(pollForSuccess); }); }; AnimationTest.prototype.setupPolledImage = function() { // Make sure the image is visible if (!this.reusingImageAsReference) { this.enableDisplay(document.getElementById(this.imageElementId)); var currentSnapshot = snapshotWindow(window, false); var result = compareSnapshots( currentSnapshot, this.referenceSnapshot, true ); this.currentSnapshotDataURI = currentSnapshot.toDataURL(); if (result[0]) { // SUCCESS! ok(true, "Animated image looks correct, at poll #" + this.pollCounter); this.outputDebugInfo( "Animated image", "animImage", this.currentSnapshotDataURI ); this.outputDebugInfo( "Reference image", "refImage", this.referenceSnapshot.toDataURL() ); this.cleanUpAndFinish(); } } else if (!gIsRefImageLoaded) { this.myImage = new Image(); this.myImage.onload = reuseImageCallback; document .getElementById(this.imageElementId) .setAttribute("src", this.referenceElementId); } }; AnimationTest.prototype.checkImage = function() { if (this.isTestFinished) { return; } this.pollCounter++; // We need this for some tests, because we need to force the // test image to be visible. if (!this.reusingImageAsReference) { this.enableDisplay(document.getElementById(this.imageElementId)); } var currentSnapshot = snapshotWindow(window, false); var result = compareSnapshots(currentSnapshot, this.referenceSnapshot, true); this.currentSnapshotDataURI = currentSnapshot.toDataURL(); if (result[0]) { // SUCCESS! ok(true, "Animated image looks correct, at poll #" + this.pollCounter); this.outputDebugInfo("Animated image", "animImage", result[1]); this.outputDebugInfo("Reference image", "refImage", result[2]); this.cleanUpAndFinish(); } }; AnimationTest.prototype.takeReferenceSnapshot = function(resolve) { this.numRefsTaken++; // Test to make sure the reference image doesn't match a clean snapshot if (!this.cleanSnapshot) { this.takeCleanSnapshot(); } // Used later to verify that the reference div disappeared if (!this.blankSnapshot) { this.takeBlankSnapshot(); } if (this.reusingImageAsReference) { // Show reference elem (which is actually our image), & take a snapshot var referenceElem = document.getElementById(this.imageElementId); this.enableDisplay(referenceElem); this.referenceSnapshot = snapshotWindow(window, false); let snapResult = compareSnapshots( this.cleanSnapshot, this.referenceSnapshot, false ); if (!snapResult[0]) { if (this.blankWaitTime > 2000) { // if it took longer than two seconds to load the image, we probably // have a problem. this.wereFailures = true; ok( snapResult[0], "Reference snapshot shouldn't match clean (non-image) snapshot" ); } else { this.blankWaitTime += currentTest.pollFreq; // let's wait a bit and see if it clears up setTimeout( () => this.takeReferenceSnapshot(resolve), currentTest.pollFreq ); return; } } ok( snapResult[0], "Reference snapshot shouldn't match clean (non-image) snapshot" ); let dataString = "Reference Snapshot #" + this.numRefsTaken; this.outputDebugInfo( dataString, "refSnapId", this.referenceSnapshot.toDataURL() ); } else { // Make sure the animation section is hidden this.disableDisplay(document.getElementById(this.imageElementId)); // Show reference div, & take a snapshot var referenceDiv = document.getElementById(this.referenceElementId); this.enableDisplay(referenceDiv); this.referenceSnapshot = snapshotWindow(window, false); let snapResult = compareSnapshots( this.cleanSnapshot, this.referenceSnapshot, false ); if (!snapResult[0]) { if (this.blankWaitTime > 2000) { // if it took longer than two seconds to load the image, we probably // have a problem. this.wereFailures = true; ok( snapResult[0], "Reference snapshot shouldn't match clean (non-image) snapshot" ); } else { this.blankWaitTime += 20; // let's wait a bit and see if it clears up setTimeout(() => this.takeReferenceSnapshot(resolve), 20); return; } } ok( snapResult[0], "Reference snapshot shouldn't match clean (non-image) snapshot" ); let dataString = "Reference Snapshot #" + this.numRefsTaken; this.outputDebugInfo( dataString, "refSnapId", this.referenceSnapshot.toDataURL() ); // Re-hide reference div, and take another snapshot to be sure it's gone this.disableDisplay(referenceDiv); this.testBlankCameBack(); } resolve(); }; AnimationTest.prototype.enableDisplay = function(element) { if (!element) { return; } if (!this.xulTest) { element.style.display = ""; } else { element.setAttribute("hidden", "false"); } }; AnimationTest.prototype.disableDisplay = function(element) { if (!element) { return; } if (!this.xulTest) { element.style.display = "none"; } else { element.setAttribute("hidden", "true"); } }; AnimationTest.prototype.testBlankCameBack = function() { var blankSnapshot2 = snapshotWindow(window, false); var result = compareSnapshots(this.blankSnapshot, blankSnapshot2, true); ok( result[0], "Reference image should disappear when it becomes display:none" ); if (!result[0]) { this.wereFailures = true; var dataString = "Second Blank Snapshot"; this.outputDebugInfo(dataString, "blank2SnapId", result[2]); } }; AnimationTest.prototype.cleanUpAndFinish = function() { // On the off chance that failTest and checkImage are triggered // back-to-back, use a flag to prevent multiple calls to SimpleTest.finish. if (this.isTestFinished) { return; } this.isTestFinished = true; // Call our closing function, if one exists if (this.closeFunc) { this.closeFunc(); return; } if (this.wereFailures) { document.getElementById(this.debugElementId).style.display = "block"; } SimpleTest.finish(); document.getElementById(this.debugElementId).style.display = ""; };