<!--
  Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/
-->
<html>
<head>
  <title>Basic Promise Test</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">

</div>
<pre id="test">
<script type="application/javascript"><!--

function promiseResolve() {
  ok(Promise, "Promise object should exist");

  var promise = new Promise(function(resolve, reject) {
    ok(resolve, "Promise.resolve exists");
    ok(reject, "Promise.reject exists");

    resolve(42);
  });
  promise.then(function(what) {
    ok(true, "Then - resolveCb has been called");
    is(what, 42, "ResolveCb received 42");
    runTest();
  }, function() {
    ok(false, "Then - rejectCb has been called");
    runTest();
  });
}

function promiseResolveNoArg() {
  var promise = new Promise(function(resolve, reject) {
    ok(resolve, "Promise.resolve exists");
    ok(reject, "Promise.reject exists");

    resolve();
  });
  promise.then(function(what) {
    ok(true, "Then - resolveCb has been called");
    is(what, undefined, "ResolveCb received undefined");
    runTest();
  }, function() {
    ok(false, "Then - rejectCb has been called");
    runTest();
  });
}

function promiseReject() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });
  promise.then(function(what) {
    ok(false, "Then - resolveCb has been called");
    runTest();
  }, function(what) {
    ok(true, "Then - rejectCb has been called");
    is(what, 42, "RejectCb received 42");
    runTest();
  });
}

function promiseRejectNoHandler() {
  // This test only checks that the code that reports unhandled errors in the
  // Promises implementation does not crash or leak.

  new Promise(function(res, rej) {
    // eslint-disable-next-line no-undef
    noSuchMethod();
  });
  runTest();
}

function promiseRejectNoArg() {
  var promise = new Promise(function(resolve, reject) {
    reject();
  });
  promise.then(function(what) {
    ok(false, "Then - resolveCb has been called");
    runTest();
  }, function(what) {
    ok(true, "Then - rejectCb has been called");
    is(what, undefined, "RejectCb received undefined");
    runTest();
  });
}

function promiseException() {
  var promise = new Promise(function(resolve, reject) {
    // eslint-disable-next-line no-throw-literal
    throw 42;
  });
  promise.then(function(what) {
    ok(false, "Then - resolveCb has been called");
    runTest();
  }, function(what) {
    ok(true, "Then - rejectCb has been called");
    is(what, 42, "RejectCb received 42");
    runTest();
  });
}

function promiseGC() {
  var resolve;
  var promise = new Promise(function(r1, r2) {
    resolve = r1;
  });
  promise.then(function(what) {
    ok(true, "Then - promise is still alive");
    runTest();
  });

  promise = null;

  SpecialPowers.gc();
  SpecialPowers.forceGC();
  SpecialPowers.forceCC();

  resolve(42);
}

function promiseAsync_TimeoutResolveThen() {
  var handlerExecuted = false;

  setTimeout(function() {
    ok(handlerExecuted, "Handler should have been called before the timeout.");

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  }, 0);

  Promise.resolve().then(function() {
    handlerExecuted = true;
  });

  ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
}

function promiseAsync_ResolveTimeoutThen() {
  var handlerExecuted = false;

  var promise = Promise.resolve();

  setTimeout(function() {
    ok(handlerExecuted, "Handler should have been called before the timeout.");

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  }, 0);

  promise.then(function() {
    handlerExecuted = true;
  });

  ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
}

function promiseAsync_ResolveThenTimeout() {
  var handlerExecuted = false;

  Promise.resolve().then(function() {
    handlerExecuted = true;
  });

  setTimeout(function() {
    ok(handlerExecuted, "Handler should have been called before the timeout.");

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  }, 0);

  ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
}

function promiseAsync_SyncXHR() {
  var handlerExecuted = false;

  Promise.resolve().then(function() {
    handlerExecuted = true;

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  });

  ok(!handlerExecuted, "Handlers are not called until the next microtask.");

  var xhr = new XMLHttpRequest();
  xhr.open("GET", "testXHR.txt", false);
  xhr.send(null);

  ok(!handlerExecuted, "Sync XHR should not trigger microtask execution.");
}

function promiseDoubleThen() {
  var steps = 0;
  var promise = new Promise(function(r1, r2) {
    r1(42);
  });

  promise.then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 42, "Value == 42");
    steps++;
  }, function(what) {
    ok(false, "Then.reject has been called");
  });

  promise.then(function(what) {
    ok(true, "Then.resolve has been called");
    is(steps, 1, "Then.resolve - step == 1");
    is(what, 42, "Value == 42");
    runTest();
  }, function(what) {
    ok(false, "Then.reject has been called");
  });
}

function promiseThenException() {
  var promise = new Promise(function(resolve, reject) {
    resolve(42);
  });

  promise.then(function(what) {
    ok(true, "Then.resolve has been called");
    // eslint-disable-next-line no-throw-literal
    throw "booh";
  }).catch(function(e) {
    ok(true, "window.onerror has been called!");
    runTest();
  });
}

function promiseThenCatchThen() {
  var promise = new Promise(function(resolve, reject) {
    resolve(42);
  });

  var promise2 = promise.then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 42, "Value == 42");
    return what + 1;
  }, function(what) {
    ok(false, "Then.reject has been called");
  });

  isnot(promise, promise2, "These 2 promise objs are different");

  promise2.then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 43, "Value == 43");
    return what + 1;
  }, function(what) {
    ok(false, "Then.reject has been called");
  }).catch(function() {
    ok(false, "Catch has been called");
  }).then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 44, "Value == 44");
    runTest();
  }, function(what) {
    ok(false, "Then.reject has been called");
  });
}

function promiseThenNoArg() {
  var promise = new Promise(function(resolve, reject) {
    resolve(42);
  });

  var clone = promise.then();
  isnot(promise, clone, "These 2 promise objs are different");
  promise.then(function(v) {
    clone.then(function(cv) {
      is(v, cv, "Both resolve to the same value");
      runTest();
    });
  });
}

function promiseThenUndefinedResolveFunction() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });

  try {
    promise.then(undefined, function(v) {
      is(v, 42, "Promise rejected with 42");
      runTest();
    });
  } catch (e) {
    ok(false, "then should not throw on undefined resolve function");
  }
}

function promiseThenNullResolveFunction() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });

  try {
    promise.then(null, function(v) {
      is(v, 42, "Promise rejected with 42");
      runTest();
    });
  } catch (e) {
    ok(false, "then should not throw on null resolve function");
  }
}

function promiseRejectThenCatchThen() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });

  var promise2 = promise.then(function(what) {
    ok(false, "Then.resolve has been called");
  }, function(what) {
    ok(true, "Then.reject has been called");
    is(what, 42, "Value == 42");
    return what + 1;
  });

  isnot(promise, promise2, "These 2 promise objs are different");

  promise2.then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 43, "Value == 43");
    return what + 1;
  }).catch(function(what) {
    ok(false, "Catch has been called");
  }).then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 44, "Value == 44");
    runTest();
  });
}

function promiseRejectThenCatchThen2() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });

  promise.then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 42, "Value == 42");
    return what + 1;
  }).catch(function(what) {
    is(what, 42, "Value == 42");
    ok(true, "Catch has been called");
    return what + 1;
  }).then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 43, "Value == 43");
    runTest();
  });
}

function promiseRejectThenCatchExceptionThen() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });

  promise.then(function(what) {
    ok(false, "Then.resolve has been called");
  }, function(what) {
    ok(true, "Then.reject has been called");
    is(what, 42, "Value == 42");
    // eslint-disable-next-line no-throw-literal
    throw (what + 1);
  }).catch(function(what) {
    ok(true, "Catch has been called");
    is(what, 43, "Value == 43");
    return what + 1;
  }).then(function(what) {
    ok(true, "Then.resolve has been called");
    is(what, 44, "Value == 44");
    runTest();
  });
}

function promiseThenCatchOrderingResolve() {
  var global = 0;
  var f = new Promise(function(r1, r2) {
    r1(42);
  });

  f.then(function() {
    f.then(function() {
      global++;
    });
    f.catch(function() {
      global++;
    });
    f.then(function() {
      global++;
    });
    setTimeout(function() {
      is(global, 2, "Many steps... should return 2");
      runTest();
    }, 0);
  });
}

function promiseThenCatchOrderingReject() {
  var global = 0;
  var f = new Promise(function(r1, r2) {
    r2(42);
  });

  f.then(function() {}, function() {
    f.then(function() {
      global++;
    });
    f.catch(function() {
      global++;
    });
    f.then(function() {}, function() {
      global++;
    });
    setTimeout(function() {
      is(global, 2, "Many steps... should return 2");
      runTest();
    }, 0);
  });
}

function promiseCatchNoArg() {
  var promise = new Promise(function(resolve, reject) {
    reject(42);
  });

  var clone = promise.catch();
  isnot(promise, clone, "These 2 promise objs are different");
  promise.catch(function(v) {
    clone.catch(function(cv) {
      is(v, cv, "Both reject to the same value");
      runTest();
    });
  });
}

function promiseNestedPromise() {
  new Promise(function(resolve, reject) {
    resolve(new Promise(function(res, rej) {
      ok(true, "Nested promise is executed");
      res(42);
    }));
  }).then(function(value) {
    is(value, 42, "Nested promise is executed and then == 42");
    runTest();
  });
}

function promiseNestedNestedPromise() {
  new Promise(function(resolve, reject) {
    resolve(new Promise(function(res, rej) {
      ok(true, "Nested promise is executed");
      res(42);
    }).then(function(what) { return what + 1; }));
  }).then(function(value) {
    is(value, 43, "Nested promise is executed and then == 43");
    runTest();
  });
}

function promiseWrongNestedPromise() {
  new Promise(function(resolve, reject) {
    resolve(new Promise(function(r, r2) {
      ok(true, "Nested promise is executed");
      r(42);
    }));
    reject(42);
  }).then(function(value) {
    is(value, 42, "Nested promise is executed and then == 42");
    runTest();
  }, function(value) {
     ok(false, "This is wrong");
  });
}

function promiseLoop() {
  new Promise(function(resolve, reject) {
    resolve(new Promise(function(res, rej) {
      ok(true, "Nested promise is executed");
      res(new Promise(function(resInner, rejInner) {
        ok(true, "Nested nested promise is executed");
        resInner(42);
      }));
    }));
  }).then(function(value) {
    is(value, 42, "Nested nested promise is executed and then == 42");
    runTest();
  }, function(value) {
     ok(false, "This is wrong");
  });
}

function promiseStaticReject() {
  var promise = Promise.reject(42);
  promise.then(function(what) {
    ok(false, "This should not be called");
  }, function(what) {
    is(what, 42, "Value == 42");
    runTest();
  });
}

function promiseStaticResolve() {
  var promise = Promise.resolve(42);
  promise.then(function(what) {
    is(what, 42, "Value == 42");
    runTest();
  }, function() {
    ok(false, "This should not be called");
  });
}

function promiseResolveNestedPromise() {
  var promise = Promise.resolve(new Promise(function(r, r2) {
    ok(true, "Nested promise is executed");
    r(42);
  }, function() {
    ok(false, "This should not be called");
  }));
  promise.then(function(what) {
    is(what, 42, "Value == 42");
    runTest();
  }, function() {
    ok(false, "This should not be called");
  });
}

function promiseSimpleThenableResolve() {
  var thenable = { then(resolve) { resolve(5); } };
  var promise = new Promise(function(resolve, reject) {
    resolve(thenable);
  });

  promise.then(function(v) {
    ok(v === 5, "promiseSimpleThenableResolve");
    runTest();
  }, function(e) {
    ok(false, "promiseSimpleThenableResolve: Should not reject");
  });
}

function promiseSimpleThenableReject() {
  var thenable = { then(resolve, reject) { reject(5); } };
  var promise = new Promise(function(resolve, reject) {
    resolve(thenable);
  });

  promise.then(function() {
    ok(false, "promiseSimpleThenableReject: Should not resolve");
    runTest();
  }, function(e) {
    ok(e === 5, "promiseSimpleThenableReject");
    runTest();
  });
}

function promiseThenableThrowsBeforeCallback() {
  var thenable = { then(resolve) {
    throw new TypeError("Hi there");

    // eslint-disable-next-line no-unreachable
    resolve(5);
  }};

  var promise = Promise.resolve(thenable);
  promise.then(function(v) {
    ok(false, "promiseThenableThrowsBeforeCallback: Should've rejected");
    runTest();
  }, function(e) {
    ok(e instanceof TypeError, "promiseThenableThrowsBeforeCallback");
    runTest();
  });
}

function promiseThenableThrowsAfterCallback() {
  var thenable = { then(resolve) {
    resolve(5);
    throw new TypeError("Hi there");
  }};

  var promise = Promise.resolve(thenable);
  promise.then(function(v) {
    ok(v === 5, "promiseThenableThrowsAfterCallback");
    runTest();
  }, function(e) {
    ok(false, "promiseThenableThrowsAfterCallback: Should've resolved");
    runTest();
  });
}

function promiseThenableRejectThenResolve() {
  var thenable = { then(resolve, reject) {
    reject(new TypeError("Hi there"));
    resolve(5);
  }};

  var promise = Promise.resolve(thenable);
  promise.then(function(v) {
    ok(false, "promiseThenableRejectThenResolve should have rejected");
    runTest();
  }, function(e) {
    ok(e instanceof TypeError, "promiseThenableRejectThenResolve");
    runTest();
  });
}

function promiseWithThenReplaced() {
  // Ensure that we call the 'then' on the promise and not the internal then.
  var promise = new Promise(function(resolve, reject) {
    resolve(5);
  });

  // Rogue `then` always rejects.
  promise.then = function(onFulfill, onReject) {
    onReject(new TypeError("Foo"));
  };

  var promise2 = Promise.resolve(promise);
  promise2.then(function(v) {
    ok(false, "promiseWithThenReplaced: Should've rejected");
    runTest();
  }, function(e) {
    ok(e instanceof TypeError, "promiseWithThenReplaced");
    runTest();
  });
}

function promiseStrictHandlers() {
  var promise = Promise.resolve(5);
  promise.then(function() {
    "use strict";
    ok(this === undefined, "Strict mode callback should have this === undefined.");
    runTest();
  });
}

function promiseStrictExecutorThisArg() {
  new Promise(function(resolve, reject) {
    "use strict";
    ok(this === undefined, "thisArg should be undefined.");
    runTest();
  });
}

function promiseResolveArray() {
  var p = Promise.resolve([1, 2, 3]);
  ok(p instanceof Promise, "Should return a Promise.");
  p.then(function(v) {
    ok(Array.isArray(v), "Resolved value should be an Array");
    is(v.length, 3, "Length should match");
    is(v[0], 1, "Resolved value should match original");
    is(v[1], 2, "Resolved value should match original");
    is(v[2], 3, "Resolved value should match original");
    runTest();
  });
}

function promiseResolveThenable() {
  var p = Promise.resolve({ then(onFulfill, onReject) { onFulfill(2); } });
  ok(p instanceof Promise, "Should cast to a Promise.");
  p.then(function(v) {
    is(v, 2, "Should resolve to 2.");
    runTest();
  }, function(e) {
    ok(false, "promiseResolveThenable should've resolved");
    runTest();
  });
}

function promiseResolvePromise() {
  var original = Promise.resolve(true);
  var cast = Promise.resolve(original);

  ok(cast instanceof Promise, "Should cast to a Promise.");
  is(cast, original, "Should return original Promise.");
  cast.then(function(v) {
    is(v, true, "Should resolve to true.");
    runTest();
  });
}

// Bug 1009569.
// Ensure that thenables are run on a clean stack asynchronously.
// Test case adopted from
// https://gist.github.com/getify/d64bb01751b50ed6b281#file-bug1-js.
function promiseResolveThenableCleanStack() {
  function immed(s) { x++; s(); }
  function incX() { x++; }

  var x = 0;
  var thenable = { then: immed };
  var results = [];

  var p = Promise.resolve(thenable).then(incX);
  results.push(x);

  // check what happens after all "next cycle" steps
  // have had a chance to complete
  setTimeout(function() {
    // Result should be [0, 2] since `thenable` will be called async.
    is(results[0], 0, "Expected thenable to be called asynchronously");
    // See Bug 1023547 comment 13 for why this check has to be gated on p.
    p.then(function() {
      results.push(x);
      is(results[1], 2, "Expected thenable to be called asynchronously");
      runTest();
    });
  }, 1000);
}

// Bug 1008467 - Promise fails with "too much recursion".
// The bug was that the callbacks passed to a thenable would resolve the
// promise synchronously when the fulfill handler returned a non-thenable.
//
// For example:
// var p = new Promise(function(resolve) {
//   resolve(5);
// });
// var m = Promise.resolve(p);
//
// At this point `m` is a Promise that is resolved with a thenable `p`, so it
// calls `p.then()` with two callbacks, both of which would synchronously resolve
// `m` when `p` invoked them (on account of itself being resolved, possibly
// synchronously. A chain of these 'Promise resolved by a Promise' would lead to
// stack overflow.
function promiseTestAsyncThenableResolution() {
  var k = 3000;
  Promise.resolve().then(function next() {
    k--;
    if (k > 0) return Promise.resolve().then(next);
    return undefined;
  }).then(function() {
    ok(true, "Resolution of a chain of thenables should not be synchronous.");
    runTest();
  });
}

// Bug 1062323
function promiseWrapperAsyncResolution() {
  var p = new Promise(function(resolve, reject) {
    resolve();
  });

  var results = [];
  var q = p.then(function() {
    results.push("1-1");
  }).then(function() {
    results.push("1-2");
  }).then(function() {
    results.push("1-3");
  });

  var r = p.then(function() {
    results.push("2-1");
  }).then(function() {
    results.push("2-2");
  }).then(function() {
    results.push("2-3");
  });

  Promise.all([q, r]).then(function() {
    var match = results[0] == "1-1" &&
                results[1] == "2-1" &&
                results[2] == "1-2" &&
                results[3] == "2-2" &&
                results[4] == "1-3" &&
                results[5] == "2-3";
                info(results);
    ok(match, "Chained promises should resolve asynchronously.");
    runTest();
  }, function() {
    ok(false, "promiseWrapperAsyncResolution: One of the promises failed.");
    runTest();
  });
}

var tests = [ promiseResolve, promiseReject,
              promiseException, promiseGC,
              promiseAsync_TimeoutResolveThen,
              promiseAsync_ResolveTimeoutThen,
              promiseAsync_ResolveThenTimeout,
              promiseAsync_SyncXHR,
              promiseDoubleThen, promiseThenException,
              promiseThenCatchThen, promiseRejectThenCatchThen,
              promiseRejectThenCatchThen2,
              promiseRejectThenCatchExceptionThen,
              promiseThenCatchOrderingResolve,
              promiseThenCatchOrderingReject,
              promiseNestedPromise, promiseNestedNestedPromise,
              promiseWrongNestedPromise, promiseLoop,
              promiseStaticReject, promiseStaticResolve,
              promiseResolveNestedPromise,
              promiseResolveNoArg,
              promiseRejectNoArg,
              promiseThenNoArg,
              promiseThenUndefinedResolveFunction,
              promiseThenNullResolveFunction,
              promiseCatchNoArg,
              promiseRejectNoHandler,
              promiseSimpleThenableResolve,
              promiseSimpleThenableReject,
              promiseThenableThrowsBeforeCallback,
              promiseThenableThrowsAfterCallback,
              promiseThenableRejectThenResolve,
              promiseWithThenReplaced,
              promiseStrictHandlers,
              promiseStrictExecutorThisArg,
              promiseResolveArray,
              promiseResolveThenable,
              promiseResolvePromise,
              promiseResolveThenableCleanStack,
              promiseTestAsyncThenableResolution,
              promiseWrapperAsyncResolution,
            ];

function runTest() {
  if (!tests.length) {
    SimpleTest.finish();
    return;
  }

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

SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("untriaged");
runTest();
// -->
</script>
</pre>
</body>
</html>
