<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Image srcset mutations</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
  <script type="application/javascript">
    "use strict";

    // Tests the relevant mutations part of the spec for <img> inside <picture> tags
    // https://html.spec.whatwg.org/#relevant-mutations
    SimpleTest.waitForExplicitFinish();

    // 50x50 png
    var testPNG50 = new URL("image_50.png", location).href;
    // 100x100 png
    var testPNG100 = new URL("image_100.png", location).href;
    // 200x200 png
    var testPNG200 = new URL("image_200.png", location).href;

    var tests = [];
    var img;
    var picture;
    var source1;
    var source2;
    var source3;
    var expectingErrors = 0;
    var expectingLoads = 0;
    var afterExpectCallback;

    function onImgLoad() {
      ok(expectingLoads > 0, "expected load");
      if (expectingLoads > 0) {
        expectingLoads--;
      }
      if (!expectingLoads && !expectingErrors) {
        setTimeout(afterExpectCallback, 0);
      }
    }
    function onImgError() {
      ok(expectingErrors > 0, "expected error");
      if (expectingErrors > 0) {
        expectingErrors--;
      }
      if (!expectingLoads && !expectingErrors) {
        setTimeout(afterExpectCallback, 0);
      }
    }
    function expectEvents(loads, errors, callback) {
      if (!loads && !errors) {
        setTimeout(callback, 0);
      } else {
        expectingLoads += loads;
        expectingErrors += errors;
        info("Waiting for " + expectingLoads + " load and " + expectingErrors + " error events");
        afterExpectCallback = callback;
      }
    }

    // Setup image outside the tree dom, make sure it loads
    tests.push(function() {
      info("test 1");
      img.srcset = testPNG100;
      img.src = testPNG50;
      is(img.currentSrc, '', "Should not have synchronously selected source");

      // No events should have fired synchronously, now we should get just one load (and no 404 error)
      expectEvents(1, 0, nextTest);
    });

    // Binding to an empty picture should trigger an event, even if source doesn't change
    tests.push(function() {
      info("test 2");
      is(img.currentSrc, testPNG100, "Should have loaded testPNG100");
      document.body.appendChild(picture);
      picture.appendChild(img);
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      expectEvents(1, 0, nextTest);
    });

    // inserting and removing an empty source before the image should both trigger a no-op reload
    tests.push(function() {
      info("test 3");
      is(img.currentSrc, testPNG100, "Should still have testPNG100");
      picture.insertBefore(source1, img);
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      // should fire one event, not change source
      expectEvents(1, 0, function() {
        is(img.currentSrc, testPNG100, "Should still have testPNG100");
        picture.removeChild(source1);
        is(img.currentSrc, testPNG100, "Should still have testPNG100");

        // Should also no-op fire
        expectEvents(1, 0, nextTest);
      });
    });

    // insert and remove valid source before
    tests.push(function() {
      info("test 4");
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      // Insert source1 before img with valid candidate
      source1.srcset = testPNG50;
      picture.insertBefore(source1, img);
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      // should fire one event, change to the source
      expectEvents(1, 0, function() {
        is(img.currentSrc, testPNG50, "Should have switched to testPNG50");
        picture.removeChild(source1);
        is(img.currentSrc, testPNG50, "Should still have testPNG50");

        // Should also no-op fire
        expectEvents(1, 0, function() {
          is(img.currentSrc, testPNG100, "Should have returned to testPNG100");
          nextTest();
        });
      });
    });

    // insert and remove valid source after
    tests.push(function() {
      info("test 5");
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      // Insert source1 before img with valid candidate
      source1.srcset = testPNG50;
      picture.appendChild(source1);
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      // should fire nothing, no action
      expectEvents(0, 0, function() {
        is(img.currentSrc, testPNG100, "Should still have testPNG100");

        // Same with removing
        picture.removeChild(source1);
        expectEvents(0, 0, function() {
          is(img.currentSrc, testPNG100, "Should still have testPNG100");
          nextTest();
        });
      });
    });

    // Should re-consider earlier sources when a load event occurs.
    tests.push(function() {
      info("test 6");

      // Insert two valid sources, with MQ causing us to select the second
      source1.srcset = testPNG50 + " 1x";
      source1.media = "(min-resolution: 2dppx)"; // Wont match, test starts at 1x
      source2.srcset = testPNG200;
      picture.insertBefore(source1, img);
      picture.insertBefore(source2, img);
      is(img.currentSrc, testPNG100, "Should still have testPNG100");

      // should get one load, selecting source2
      expectEvents(1, 0, function() {
        is(img.currentSrc, testPNG200, "Should have selected testPNG200");

        expectEvents(1, 0, function() {
          is(img.currentSrc, testPNG50, "Should have switched to testPNG50");

          // Now add a source *also* wanting that DPI *just before* the
          // selected source. Properly re-running the algorithm should
          // re-consider all sources and thus go back to the first
          // source, not just the valid source just inserted before us.
          source3.media = source1.media;
          source3.srcset = testPNG100;
          picture.insertBefore(source3, source2);
          // This should trigger a reload, but we should re-consider
          // source1 and remain with that, not just the newly added source2
          expectEvents(1, 0, function() {
            is(img.currentSrc, testPNG50, "Should have remained on testPNG50");
            expectEvents(0, 0, nextTest);
          });
        });

        // Switch DPI to match the first source.
        SpecialPowers.pushPrefEnv({'set': [ ["layout.css.devPixelsPerPx", "2.0"] ] });
      });
    });

    // insert and remove valid source after our current source should
    // trigger a reload, but not switch source
    tests.push(function() {
      info("test 7");
      // Should be using source1 from last test
      is(img.currentSrc, testPNG50, "Should still have testPNG50");

      // Remove source2, should trigger an event even though we would
      // not switch

      picture.removeChild(source2);
      expectEvents(1, 0, function() {
        is(img.currentSrc, testPNG50, "Should still have testPNG50");

        // Same with re-adding
        picture.insertBefore(source2, img);
        expectEvents(1, 0, function() {
          is(img.currentSrc, testPNG50, "Should still have testPNG50");
          expectEvents(0, 0, nextTest);
        });
      });
    });

    // Changing source attributes should trigger updates
    tests.push(function() {
      info("test 8");
      // Should be using source1 from last test
      is(img.currentSrc, testPNG50, "Should still have testPNG50");

      // Reconfigure source1 to have empty srcset. Should switch to
      // next source due to becoming invalid.
      source1.srcset = "";
      is(img.currentSrc, testPNG50, "Should still have testPNG50");

      expectEvents(1, 0, function() {
        is(img.currentSrc, testPNG100, "Should have switched to testPNG100");

        // Give source1 valid sizes. Should trigger an event but not switch anywhere.
        source1.sizes = "100vw";

        expectEvents(1, 0, function() {
          is(img.currentSrc, testPNG100, "Should still have testPNG100");

          // And a valid MQ
          source1.media = "(min-resolution: 1dppx)";
          expectEvents(1, 0, function() {
            // And a valid type...
            source1.type = "image/png";
            expectEvents(1, 0, function() {
              // Finally restore srcset, should trigger load and re-consider source1 which is valid again
              source1.srcset = testPNG50;
              expectEvents(1, 0, function() {
                is(img.currentSrc, testPNG50, "Should have selected testPNG50");
                expectEvents(0, 0, nextTest);
              });
            });
          });
        });
      });
    });

    // Inserting a source after <img> and touching its attributes should all be no-ops
    tests.push(function() {
      info("test 9");
      // Setup: source2
      picture.removeChild(source2);

      expectEvents(1, 0, function() {
        is(img.currentSrc, testPNG50, "Should still have testPNG50");

        source2.srcset = testPNG200;
        source2.media = "";
        source2.sizes = "100vw";
        source2.type = "image/png";
        // Append valid source
        picture.appendChild(source2);
        // Touch all the things (but keep it valid)
        source2.srcset = testPNG100;
        source2.media = "(min-resolution: 2dppx)";
        source2.sizes = "50vw";
        source2.type = "image/png";

        // No event should fire. Source should not change.
        expectEvents(0, 0, function() {
          is(img.currentSrc, testPNG50, "Should still have testPNG50");
          expectEvents(0, 0, nextTest);
        });
      });
    });

    function nextTest() {
      if (tests.length) {
        // Spin event loop to make sure no unexpected image events are
        // pending (unexpected events will assert in the handlers)
        setTimeout(function() {
          (tests.shift())();
        }, 0);
      } else {
        // We'll get a flood of load events due to prefs being popped while cleaning up.
        // Ignore it all.
        img.removeEventListener("load", onImgLoad);
        img.removeEventListener("error", onImgError);
        SimpleTest.finish();
      }
    }

    addEventListener("load", function() {
      SpecialPowers.pushPrefEnv({'set': [["layout.css.devPixelsPerPx", "1.0" ]] },
                                function() {
        // Create these after the pref is set, as it is guarding webIDL attributes
        img = document.createElement("img");
        img.addEventListener("load", onImgLoad);
        img.addEventListener("error", onImgError);
        picture = document.createElement("picture");
        source1 = document.createElement("source");
        source2 = document.createElement("source");
        source3 = document.createElement("source");
        setTimeout(nextTest, 0);
      });
    });
  </script>
</body>
</html>
