<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>Tests tabs.captureTab and tabs.captureVisibleTab</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
  <script type="text/javascript" src="head.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";

async function runTest({ html, fullZoom, coords, rect, scale }) {
  let url = `data:text/html,${encodeURIComponent(html)}#scroll`;

  async function background({ coords, rect, scale, method, fullZoom }) {
    try {
      // Wait for the page to load
      await new Promise(resolve => {
        browser.webNavigation.onCompleted.addListener(
          () => resolve(),
          {url: [{schemes: ["data"]}]});
      });

      let [tab] = await browser.tabs.query({
        currentWindow: true,
        active: true,
      });

      // TODO: Bug 1665429 - on mobile we ignore zoom for now
      if (browser.tabs.setZoom) {
        await browser.tabs.setZoom(tab.id, fullZoom ?? 1);
      }

      let id = method === "captureVisibleTab" ? tab.windowId : tab.id;

      let [jpeg, png, ...pngs] = await Promise.all([
        browser.tabs[method](id, { format: "jpeg", quality: 95, rect, scale }),
        browser.tabs[method](id, { format: "png", quality: 95, rect, scale }),
        browser.tabs[method](id, { quality: 95, rect, scale }),
        browser.tabs[method](id, { rect, scale }),
      ]);

      browser.test.assertTrue(
        pngs.every(url => url == png),
        "All PNGs are identical"
      );

      browser.test.assertTrue(
        jpeg.startsWith("data:image/jpeg;base64,"),
        "jpeg is JPEG"
      );
      browser.test.assertTrue(
        png.startsWith("data:image/png;base64,"),
        "png is PNG"
      );

      let promises = [jpeg, png].map(
        url =>
          new Promise(resolve => {
            let img = new Image();
            img.src = url;
            img.onload = () => resolve(img);
          })
      );

      let width = (rect?.width ?? tab.width) * (scale ?? devicePixelRatio);
      let height = (rect?.height ?? tab.height) * (scale ?? devicePixelRatio);

      [jpeg, png] = await Promise.all(promises);
      let images = { jpeg, png };
      for (let format of Object.keys(images)) {
        let img = images[format];

        // WGP.drawSnapshot() deals in int coordinates, and rounds down.
        browser.test.assertTrue(
          Math.abs(width - img.width) <= 1,
          `${format} ok image width: ${img.width}, expected: ${width}`
        );
        browser.test.assertTrue(
          Math.abs(height - img.height) <= 1,
          `${format} ok image height ${img.height}, expected: ${height}`
        );

        let canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        canvas.mozOpaque = true;

        let ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0);

        for (let { x, y, color } of coords) {
          x = (x + img.width) % img.width;
          y = (y + img.height) % img.height;
          let imageData = ctx.getImageData(x, y, 1, 1).data;

          if (format == "png") {
            browser.test.assertEq(
              `rgba(${color},255)`,
              `rgba(${[...imageData]})`,
              `${format} image color is correct at (${x}, ${y})`
            );
          } else {
            // Allow for some deviation in JPEG version due to lossy compression.
            const SLOP = 3;

            browser.test.log(
              `Testing ${format} image color at (${x}, ${y}), have rgba(${[
                ...imageData,
              ]}), expecting approx. rgba(${color},255)`
            );

            browser.test.assertTrue(
              Math.abs(color[0] - imageData[0]) <= SLOP,
              `${format} image color.red is correct at (${x}, ${y})`
            );
            browser.test.assertTrue(
              Math.abs(color[1] - imageData[1]) <= SLOP,
              `${format} image color.green is correct at (${x}, ${y})`
            );
            browser.test.assertTrue(
              Math.abs(color[2] - imageData[2]) <= SLOP,
              `${format} image color.blue is correct at (${x}, ${y})`
            );
            browser.test.assertEq(
              255,
              imageData[3],
              `${format} image color.alpha is correct at (${x}, ${y})`
            );
          }
        }
      }

      browser.test.notifyPass("captureTab");
    } catch (e) {
      browser.test.fail(`Error: ${e} :: ${e.stack}`);
      browser.test.notifyFail("captureTab");
    }
  }

  for (let method of ["captureTab", "captureVisibleTab"]) {
    let options = { coords, rect, scale, method, fullZoom };
    info(`Testing configuration: ${JSON.stringify(options)}`);

    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        permissions: ["<all_urls>", "webNavigation"],
      },

      background: `(${background})(${JSON.stringify(options)})`,
    });

    await extension.startup();

    let testWindow = window.open(url);
    await extension.awaitFinish("captureTab");

    testWindow.close();
    await extension.unload();
  }
}

async function testEdgeToEdge({ color, fullZoom }) {
  let neutral = [0xaa, 0xaa, 0xaa];

  let html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body style="background-color: rgb(${color})">
      <!-- Fill most of the image with a neutral color to test edge-to-edge scaling. -->
      <div style="position: absolute;
                  left: 2px;
                  right: 2px;
                  top: 2px;
                  bottom: 2px;
                  background: rgb(${neutral});"></div>
    </body>
    </html>
  `;

  // Check the colors of the first and last pixels of the image, to make
  // sure we capture the entire frame, and scale it correctly.
  let coords = [
    { x: 0, y: 0, color },
    { x: -1, y: -1, color },
    { x: 300, y: 200, color: neutral },
  ];

  info(`Test edge to edge color ${color} at fullZoom=${fullZoom}`);
  await runTest({ html, fullZoom, coords });
}

add_task(async function testCaptureEdgeToEdge() {
  await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 1 });
  await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 2 });
  await testEdgeToEdge({ color: [0, 0, 0], fullZoom: 0.5 });
  await testEdgeToEdge({ color: [255, 255, 255], fullZoom: 1 });
});

const tallDoc = `<!DOCTYPE html>
  <meta charset=utf-8>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <div style="background: yellow; width: 50%; height: 500px;"></div>
  <div id=scroll style="background: red; width: 25%; height: 5000px;"></div>
  Opened with the #scroll fragment, scrolls the div ^ into view.
`;

// Test currently visible viewport is captured if scrolling is involved.
add_task(async function testScrolledViewport() {
  await runTest({
    html: tallDoc,
    coords: [
      { x: 50, y: 50, color: [255, 0, 0] },
      { x: 50, y: -50, color: [255, 0, 0] },
      { x: -50, y: -50, color: [255, 255, 255] },
    ],
  });
});

// Test rect and scale options.
add_task(async function testRectAndScale() {
  await runTest({
    html: tallDoc,
    rect: { x: 50, y: 50, width: 10, height: 1000 },
    scale: 4,
    coords: [
      { x: 0, y: 0, color: [255, 255, 0] },
      { x: -1, y: 0, color: [255, 255, 0] },
      { x: 0, y: -1, color: [255, 0, 0] },
      { x: -1, y: -1, color: [255, 0, 0] },
    ],
  });
});

// Test OOP iframes are captured, for Fission compatibility.
add_task(async function testOOPiframe() {
  await runTest({
    html: `<!DOCTYPE html>
      <meta charset=utf-8>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <iframe src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green.html"></iframe>
    `,
    coords: [
      { x: 50, y: 50, color: [0, 255, 0] },
      { x: 50, y: -50, color: [255, 255, 255] },
      { x: -50, y: 50, color: [255, 255, 255] },
    ],
  });
});

add_task(async function testOOPiframeScale() {
  let scale = 2;
  await runTest({
    html: `<!DOCTYPE html>
      <meta charset=utf-8>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <style>
        body {
          background: yellow;
          margin: 0;
        }
      </style>
      <iframe frameborder="0" style="width: 300px; height: 300px" src="http://example.net/tests/toolkit/components/extensions/test/mochitest/file_green_blue.html"></iframe>
    `,
    coords: [
      { x: 20 * scale, y: 20 * scale, color: [0, 255, 0] },
      { x: 200 * scale, y: 20 * scale, color: [0, 0, 255] },
      { x: 20 * scale, y: 200 * scale, color: [0, 0, 255] },
    ],
    scale,
  });
});

add_task(async function testCaptureTabPermissions() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["tabs"],
    },

    background() {
      browser.test.assertEq(
        undefined,
        browser.tabs.captureTab,
        'Extension without "<all_urls>" permission should not have access to captureTab'
      );
      browser.test.notifyPass("captureTabPermissions");
    },
  });

  await extension.startup();
  await extension.awaitFinish("captureTabPermissions");
  await extension.unload();
});

add_task(async function testCaptureVisibleTabPermissions() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["tabs"],
    },

    background() {
      browser.test.assertEq(
        undefined,
        browser.tabs.captureVisibleTab,
        'Extension without "<all_urls>" permission should not have access to captureVisibleTab'
      );
      browser.test.notifyPass("captureVisibleTabPermissions");
    },
  });

  await extension.startup();
  await extension.awaitFinish("captureVisibleTabPermissions");
  await extension.unload();
});
</script>
</body>
</html>
