<!DOCTYPE HTML>
<html>
<head>
  <title>Test for webkitdirectory and webkitRelativePath</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>

<body>
<input id="inputFileWebkitDirectory" type="file" webkitdirectory></input>
<input id="inputFileWebkitFile" type="file"></input>
<input id="inputFileDirectoryChange" type="file" webkitdirectory></input>

<script type="application/javascript">

const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

let promptHandler;

function waitForEvent(element, eventName) {
  return new Promise(function(resolve) {
    element.addEventListener(eventName, e => resolve(e.detail), { once: true });
  });
}

function waitForPromptHandled() {
  return new Promise(resolve => promptHandler.addMessageListener("promptAccepted", resolve));
}

// Populate the given input type=file `aInputFile`'s `files` attribute by:
// - loading `script_fileList.js` in the parent process
// - telling it to generate the "test" template directory pattern which will
//   create "foo.txt", "subdir/bar.txt", and if symlinks are available on the
//   platform, "symlink.txt" which will be a symlink to "foo.txt".  (Note that
//   we explicitly expect the symlink to be filtered out if generated, and
//   during the enhancement of the test we verified the file was created on
//   linux by running the test before fixing the GetFilesHelper logic to filter
//   the symlink out and verifying the subsequent `test_fileList` check failed.)
// - Triggering the mock file picker with the base directory of the "test"
//   template directory.
//
// It's expected that `test_fileList` will be used after this step completes in
// order to validate the results.
function populateInputFile(aInputFile) {
  var url = SimpleTest.getTestFileURL("script_fileList.js");
  var script = SpecialPowers.loadChromeScript(url);

  var MockFilePicker = SpecialPowers.MockFilePicker;
  MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);

  async function onOpened(message) {
    MockFilePicker.useDirectory(message.dir);

    let input = document.getElementById(aInputFile);
    input.setAttribute("data-name", message.name);

    let promptHandled = waitForPromptHandled();
    let changeEvent = waitForEvent(input, "change");

    input.click();

    await promptHandled;
    await changeEvent;

    MockFilePicker.cleanup();
    script.destroy();
    next();
  }

  script.addMessageListener("dir.opened", onOpened);
  script.sendAsyncMessage("dir.open", { path: "test" });
}

function checkFile(file, fileList, dirName) {
  for (var i = 0; i < fileList.length; ++i) {
    ok(fileList[i] instanceof File, "We want just files.");
    if (fileList[i].name == file.name) {
      is(fileList[i].webkitRelativePath, dirName + file.path, "Path matches");
      return;
    }
  }

  ok(false, "File not found.");
}

// Validate the contents of the given input type=file `aInputFile`'s' `files`
// property against the expected list of files `aWhat`.
function test_fileList(aInputFile, aWhat) {
  var input = document.getElementById(aInputFile);
  var fileList = input.files;

  if (aWhat == null) {
    is(fileList, null, "We want a null fileList for " + aInputFile);
    next();
    return;
  }

  is(fileList.length, aWhat.length, "We want just " + aWhat.length + " elements for " + aInputFile);
  for (var i = 0; i < aWhat.length; ++i) {
    checkFile(aWhat[i], fileList, input.dataset.name);
  }

  next();
}

// Verify that we can explicitly select a symlink and it will not be filtered
// out.  This is really a verification that GetFileHelper's file-handling logic
// https://searchfox.org/mozilla-central/rev/065102493dfc49234120c37fc6a334a5b1d86d9e/dom/filesystem/GetFilesHelper.cpp#81-86
// does not proactively take an action to filter out a selected symlink.
//
// This is a glass box test that is not entirely realistic for our actual system
// file pickers but does reflect what will happen in the drag-and-drop case
// for `HTMLInputElement::MozSetDndFilesAndDirectories` and this helps ensure
// that future implementation changes will behave as expected.  Specifically,
// the presence of webkitdirectory will result in the file picker using
// `modeGetFolder` which will only allow selection of a directory and forbid
// file selection.
//
// This test explicitly does not validate HTMLInputElement's non-webkitdirectory
// file selection mechanism because it does not involve GetFileHelper.
async function test_individualSymlink(aInputFile) {
  const input = document.getElementById(aInputFile);

  // -- Create the symlink and get a `File` instance pointing at it.
  const url = SimpleTest.getTestFileURL("script_fileList.js");
  const script = SpecialPowers.loadChromeScript(url);

  let opened = new Promise(resolve => script.addMessageListener("symlink.opened", resolve));
  script.sendAsyncMessage("symlink.open", {});
  let { dir, file: symlinkFile } = await opened;
  info(`symlink.open provided dir: ${dir}`)

  // -- Have the picker pick it
  var MockFilePicker = SpecialPowers.MockFilePicker;
  MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeOpen);

  MockFilePicker.displayDirectory = dir;
  let pickerShown = new Promise(resolve => {
    MockFilePicker.showCallback = function() {
      // This is where we are diverging from a realistic scenario in order to get
      // the expected coverage.
      MockFilePicker.setFiles([symlinkFile]);
      resolve();
    }
  });
  MockFilePicker.returnValue = MockFilePicker.returnOK;

  let changeEvent = waitForEvent(input, "change");

  input.click();

  await pickerShown;
  await changeEvent;

  MockFilePicker.cleanup();
  script.destroy();

  // -- Verify that we see the symlink.
  let fileList = input.files;
  is(fileList.length, 1, "There should be 1 file.");
  is(fileList[0].name, "symlink.txt", "The file should be the symlink.");
  next();
}

function test_webkitdirectory_attribute() {
  var a = document.createElement("input");
  a.setAttribute("type", "file");

  ok("webkitdirectory" in a, "HTMLInputElement.webkitdirectory exists");

  ok(!a.hasAttribute("webkitdirectory"), "No webkitdirectory DOM attribute by default");
  ok(!a.webkitdirectory, "No webkitdirectory attribute by default");

  a.webkitdirectory = true;

  ok(a.hasAttribute("webkitdirectory"), "Webkitdirectory DOM attribute is set");
  ok(a.webkitdirectory, "Webkitdirectory attribute is set");

  next();
}

function test_changeDataWhileWorking() {
  var url = SimpleTest.getTestFileURL("script_fileList.js");
  var script = SpecialPowers.loadChromeScript(url);

  var MockFilePicker = SpecialPowers.MockFilePicker;
  MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
  let promptHandled;

  // Let's start retrieving the root nsIFile object
  new Promise(function(resolve) {
    function onOpened(message) {
      script.removeMessageListener("dir.opened", onOpened);
      resolve(message.dir);
    }

    script.addMessageListener("dir.opened", onOpened);
    script.sendAsyncMessage("dir.open", { path: "root" });
  })

  // input.click() pointing to the root dir
  .then(async function(aDir) {
    MockFilePicker.cleanup();
    MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
    MockFilePicker.useDirectory(aDir);
    var input = document.getElementById("inputFileDirectoryChange");

    promptHandled = waitForPromptHandled();
    input.click();
  })

  // Before onchange, let's take the 'test' directory
  .then(function() {
    return new Promise(function(resolve) {
      function onOpened(message) {
        script.removeMessageListener("dir.opened", onOpened);
        script.destroy();
        resolve(message.dir);
      }

      script.addMessageListener("dir.opened", onOpened);
      script.sendAsyncMessage("dir.open", { path: "test" });
    });
  })

  // Now let's click again and wait for onchange.
  .then(async function(aDir) {
      MockFilePicker.cleanup();
      MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder);
      MockFilePicker.useDirectory(aDir);

      let input = document.getElementById("inputFileDirectoryChange");
      let changeEvent = waitForEvent(input, "change");

      input.click();

      await promptHandled;
      await changeEvent;

      MockFilePicker.cleanup();
  })
  .then(function() {
    test_fileList("inputFileWebkitDirectory", testDirData);
  });
}

async function test_setup() {
  let promptHandlerUrl = SimpleTest.getTestFileURL("script_promptHandler.js")
  promptHandler = SpecialPowers.loadChromeScript(promptHandlerUrl);

  let promptHandlerReady = new Promise(resolve => promptHandler.addMessageListener("initDone", resolve));
  promptHandler.sendAsyncMessage("init");
  await promptHandlerReady;

  SpecialPowers.pushPrefEnv({"set": [["dom.filesystem.pathcheck.disabled", true],
                                     ["dom.webkitBlink.dirPicker.enabled", true]]}, next);
}

async function test_cleanup() {
  let promptHandlerDone = new Promise(resolve => promptHandler.addMessageListener("cleanupDone", resolve));
  promptHandler.sendAsyncMessage("cleanup");
  await promptHandlerDone;
  promptHandler.destroy();
}

var testDirData = [ { name: "foo.txt", path: "/foo.txt" },
                    { name: "bar.txt", path: "/subdir/bar.txt" }];

var tests = [
  test_setup,

  function() { populateInputFile("inputFileWebkitDirectory"); },

  function() { test_fileList("inputFileWebkitDirectory", testDirData); },

  function() {
    // Symlinks are not available on Windows and so will not be created.
    if (AppConstants.platform === "win" || AppConstants.platform === "android") {
      info("Skipping individual symlink check on Windows and Android.");
      next();
      return;
    }

    test_individualSymlink("inputFileWebkitFile").catch(err => ok(false, `Problem in symlink case: ${err}`));
  },

  test_webkitdirectory_attribute,

  test_changeDataWhileWorking,
];

async function next() {
  if (!tests.length) {
    await test_cleanup();
    SimpleTest.finish();
    return;
  }

  var test = tests.shift();
  await test();
}

SimpleTest.waitForExplicitFinish();
next();
</script>
</body>
</html>
