<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=686905
-->
<head>
  <title>Test that animated images can be discarded</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
  <script type="text/javascript" src="imgutils.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686905">Mozilla Bug 686905</a>
<p id="display"></p>
<div id="content">
  <div id="container">
    <canvas id="canvas" width="100" height="100"></canvas>
    <img id="infinitepng" src="infinite-apng.png">
    <img id="infinitegif" src="animated1.gif">
    <img id="infinitewebp" src="infinite.webp">
    <img id="infiniteavif" src="infinite.avif">
    <img id="finitepng" src="restore-previous.png">
    <img id="finitegif" src="animated-gif.gif">
    <img id="finitewebp" src="keep.webp">
    <img id="finiteavif" src="animated-avif.avif">
  </div>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">

/** Test for Bug 686905. **/
SimpleTest.waitForExplicitFinish();

var gFinished = false;

var gNumDiscards = 0;

window.onload = function() {
  // Enable discarding for the test.
  SpecialPowers.pushPrefEnv({
    'set':[['image.mem.discardable',true],
           ['image.webp.enabled',true],
           ['image.avif.sequence.enabled',true]]
  }, runTest);
}

var gImgs = ['infinitepng', 'infinitegif', 'infinitewebp', 'infiniteavif',
             'finitepng',   'finitegif',   'finitewebp', 'finiteavif'];
// If we are currently counting frame updates.
var gCountingFrameUpdates = false;
// The number of frame update notifications for the images in gImgs that happen
// after discarding. (The last two images are finite looping so we don't expect
// them to get incremented but it's possible if they don't finish their
// animation before we discard them.)
var gNumFrameUpdates = [0, 0, 0, 0, 0, 0];
// The last snapshot of the image. Used to check that the image actually changes.
var gLastSnapShot = [null, null, null, null, null, null];
// Number of observed changes in the snapshot.
var gNumSnapShotChanges = [0, 0, 0, 0, 0, 0];
// If we've removed the observer.
var gRemovedObserver = [false, false, false, false, false, false];

// 2 would probably be a good enough test, we arbitrarily choose 4.
var kNumFrameUpdatesToExpect = 4;

function runTest() {
  var animatedDiscardable =
    SpecialPowers.getBoolPref('image.mem.animated.discardable');
  if (!animatedDiscardable) {
    ok(true, "discarding of animated images is disabled, nothing to test");
    SimpleTest.finish();
    return;
  }

  setTimeout(step2, 0);
}

function step2() {
  // Draw the images to canvas to force them to be decoded.
  for (let i = 0; i < gImgs.length; i++) {
    drawCanvas(document.getElementById(gImgs[i]));
  }

  for (let i = 0; i < gImgs.length; i++) {
    addCallbacks(document.getElementById(gImgs[i]), i);
  }

  setTimeout(step3, 0);
}

function step3() {
  document.getElementById("container").style.display = "none";
  document.documentElement.offsetLeft; // force that style to take effect

  for (var i = 0; i < gImgs.length; i++) {
    requestDiscard(document.getElementById(gImgs[i]));
  }

  // the discard observers will call step4
}

function step4() {
  gCountingFrameUpdates = true;
  document.getElementById("container").style.display = "";

  // Draw the images to canvas to force them to be decoded again.
  for (var i = 0; i < gImgs.length; i++) {
    drawCanvas(document.getElementById(gImgs[i]));
  }
}

function checkIfFinished() {
  if (gFinished) {
    return;
  }

  if ((gNumFrameUpdates[0] >= kNumFrameUpdatesToExpect) &&
      (gNumFrameUpdates[1] >= kNumFrameUpdatesToExpect) &&
      (gNumFrameUpdates[2] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[0] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[1] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[2] >= kNumFrameUpdatesToExpect)) {
    ok(true, "got expected frame updates");
    gFinished = true;
    SimpleTest.finish();
  }
}

// arrayIndex is the index into the arrays gNumFrameUpdates and gNumDecodes
// to increment when a frame update notification is received.
function addCallbacks(anImage, arrayIndex) {
  var observer = new ImageDecoderObserverStub();
  observer.discard = function () {
    gNumDiscards++;
    ok(true, "got image discard");
    if (arrayIndex >= 3) {
      // The last two images are finite, so we don't expect any frame updates,
      // this image is done the test, so remove the observer.
      if (!gRemovedObserver[arrayIndex]) {
        gRemovedObserver[arrayIndex] = true;
        imgLoadingContent.removeObserver(scriptedObserver);
      }
    }
    if (gNumDiscards == gImgs.length) {
      step4();
    }
  };
  observer.frameUpdate = function () {
    if (!gCountingFrameUpdates) {
      return;
    }

    // Do this off a setTimeout since nsImageLoadingContent uses a scriptblocker
    // when it notifies us and calling drawWindow can call will paint observers
    // which can dispatch a scrollport event, and events assert if dispatched
    // when there is a scriptblocker.
    setTimeout(function () {
      gNumFrameUpdates[arrayIndex]++;

      var r = document.getElementById(gImgs[arrayIndex]).getBoundingClientRect();
      var snapshot = snapshotRect(window, r, "rgba(0,0,0,0)");
      if (gLastSnapShot[arrayIndex] != null) {
        if (snapshot.toDataURL() != gLastSnapShot[arrayIndex].toDataURL()) {
          gNumSnapShotChanges[arrayIndex]++;
        }
      }
      gLastSnapShot[arrayIndex] = snapshot;

      if (gNumFrameUpdates[arrayIndex] >= kNumFrameUpdatesToExpect &&
          gNumSnapShotChanges[arrayIndex] >= kNumFrameUpdatesToExpect) {
        if (!gRemovedObserver[arrayIndex]) {
          gRemovedObserver[arrayIndex] = true;
          imgLoadingContent.removeObserver(scriptedObserver);
        }
      }
      if (!gFinished) {
        // because we do this in a setTimeout we can have several in flight
        // so don't call ok if we've already finished.
        ok(true, "got frame update");
      }
      checkIfFinished();
    }, 0);
  };
  observer = SpecialPowers.wrapCallbackObject(observer);

  var scriptedObserver = SpecialPowers.Cc["@mozilla.org/image/tools;1"]
                           .getService(SpecialPowers.Ci.imgITools)
                           .createScriptedObserver(observer);

  var imgLoadingContent = SpecialPowers.wrap(anImage);
  imgLoadingContent.addObserver(scriptedObserver);
}

function requestDiscard(anImage) {
  var request = SpecialPowers.wrap(anImage)
      .getRequest(SpecialPowers.Ci.nsIImageLoadingContent.CURRENT_REQUEST);
  setTimeout(() => request.requestDiscard(), 0);
}

function drawCanvas(anImage) {
  var canvas = document.getElementById('canvas');
  var context = canvas.getContext('2d');

  context.clearRect(0,0,100,100);
  var cleared = canvas.toDataURL();

  context.drawImage(anImage, 0, 0);
  ok(true, "we got through the drawImage call without an exception being thrown");

  ok(cleared != canvas.toDataURL(), "drawImage drew something");
}

</script>
</pre>
</body>
</html>

