From: <rb...@us...> - 2017-10-06 12:00:06
|
Revision: 14858 http://sourceforge.net/p/htmlunit/code/14858 Author: rbri Date: 2017-10-06 12:00:03 +0000 (Fri, 06 Oct 2017) Log Message: ----------- rewritten Promise support (wip) Modified Paths: -------------- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/Promise.java trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/PromiseTest.java Modified: trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/Promise.java =================================================================== --- trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/Promise.java 2017-10-04 10:13:15 UTC (rev 14857) +++ trunk/htmlunit/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/Promise.java 2017-10-06 12:00:03 UTC (rev 14858) @@ -18,7 +18,6 @@ import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE; import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF; -import com.gargoylesoftware.htmlunit.javascript.FunctionWrapper; import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine; import com.gargoylesoftware.htmlunit.javascript.PostponedAction; import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable; @@ -27,12 +26,15 @@ import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction; import com.gargoylesoftware.htmlunit.javascript.configuration.JsxStaticFunction; +import net.sourceforge.htmlunit.corejs.javascript.BaseFunction; import net.sourceforge.htmlunit.corejs.javascript.Context; import net.sourceforge.htmlunit.corejs.javascript.Function; import net.sourceforge.htmlunit.corejs.javascript.JavaScriptException; import net.sourceforge.htmlunit.corejs.javascript.NativeArray; import net.sourceforge.htmlunit.corejs.javascript.NativeObject; +import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime; import net.sourceforge.htmlunit.corejs.javascript.Scriptable; +import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject; import net.sourceforge.htmlunit.corejs.javascript.Undefined; /** @@ -40,16 +42,20 @@ * * @author Ahmed Ashour * @author Marc Guillemot + * @author Ronald Brill */ @JsxClass({CHROME, FF, EDGE}) public class Promise extends SimpleScriptable { + private enum PromiseState { PENDING, FULFILLED, REJECTED } + private PromiseState state_ = PromiseState.PENDING; private Object value_; /** To be set only by {@link #all(Context, Scriptable, Object[], Function)}. */ private Promise[] all_; - private boolean resolve_ = true; - private String exceptionDetails_; + private PostponedAction thenAction_; + private Promise linkedPromise_; + /** * Default constructor. */ @@ -72,15 +78,39 @@ */ @JsxConstructor public Promise(final Object object) { - if (object instanceof Promise) { - value_ = ((Promise) object).value_; + if (!(object instanceof Function)) { + throw ScriptRuntime.typeError("Promise resolver is not a function"); } - else if (object instanceof NativeObject) { - final NativeObject nativeObject = (NativeObject) object; - value_ = nativeObject.get("then", nativeObject); + + final Function fun = (Function) object; + final Window window = getWindow(fun); + this.setParentScope(window); + this.setPrototype(window.getPrototype(this.getClass())); + final Promise thisPromise = this; + + final Function resolve = new BaseFunction(window, ScriptableObject.getFunctionPrototype(window)) { + @Override + public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, + final Object[] args) { + thisPromise.update(PromiseState.FULFILLED, args.length != 0 ? args[0] : Undefined.instance); + return thisPromise; + } + }; + + final Function reject = new BaseFunction(window, ScriptableObject.getFunctionPrototype(window)) { + @Override + public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, + final Object[] args) { + thisPromise.update(PromiseState.REJECTED, args.length != 0 ? args[0] : Undefined.instance); + return thisPromise; + } + }; + + try { + fun.call(Context.getCurrentContext(), window, window, new Object[] {resolve, reject}); } - else { - value_ = object; + catch (final JavaScriptException e) { + thisPromise.update(PromiseState.REJECTED, e.getValue()); } } @@ -96,11 +126,7 @@ @JsxStaticFunction public static Promise resolve(final Context context, final Scriptable thisObj, final Object[] args, final Function function) { - final Promise promise = new Promise(args.length != 0 ? args[0] : Undefined.instance); - promise.setResolve(true); - promise.setParentScope(thisObj.getParentScope()); - promise.setPrototype(getWindow(thisObj).getPrototype(promise.getClass())); - return promise; + return create(thisObj, args, PromiseState.FULFILLED); } /** @@ -115,40 +141,60 @@ @JsxStaticFunction public static Promise reject(final Context context, final Scriptable thisObj, final Object[] args, final Function function) { - final Promise promise = new Promise(args.length != 0 ? args[0] : Undefined.instance); - promise.setResolve(false); + return create(thisObj, args, PromiseState.REJECTED); + } + + private static Promise create(final Scriptable thisObj, final Object[] args, final PromiseState state) { + if (args.length != 0 && args[0] instanceof Promise && state == PromiseState.FULFILLED) { + return (Promise) args[0]; + } + + final Promise promise; + if (args.length > 0) { + final Object arg = args[0]; + if (arg instanceof NativeObject) { + final NativeObject nativeObject = (NativeObject) arg; + promise = new Promise(nativeObject.get("then", nativeObject)); + } + else { + promise = new Promise(); + promise.value_ = arg; + promise.state_ = state; + } + } + else { + promise = new Promise(); + promise.value_ = Undefined.instance; + promise.state_ = state; + } + promise.setParentScope(thisObj.getParentScope()); promise.setPrototype(getWindow(thisObj).getPrototype(promise.getClass())); return promise; } - private void setResolve(final boolean resolve) { - resolve_ = resolve; - } + private void update(final PromiseState newState, final Object newValue) { + if (state_ == newState || state_ != PromiseState.PENDING) { + return; + } - /** - * Also sets the value of this promise. - */ - private boolean isResolved(final Function onRejected) { - if (all_ != null) { - final Object[] values = new Object[all_.length]; - for (int i = 0; i < all_.length; i++) { - final Promise p = all_[i]; - if (!p.isResolved(onRejected)) { - value_ = p.value_; - return false; - } + value_ = newValue; + state_ = newState; - if (p.value_ instanceof Function) { - // TODO - } - else { - values[i] = p.value_; - } + if (thenAction_ != null) { + try { + thenAction_.execute(); } - value_ = Context.getCurrentContext().newArray(getParentScope(), values); + catch (final Exception e) { + // ignore for now + } + thenAction_ = null; } - return resolve_; + + if (linkedPromise_ != null) { + linkedPromise_.update(newState, newValue); + linkedPromise_ = null; + } } /** @@ -165,7 +211,7 @@ public static Promise all(final Context context, final Scriptable thisObj, final Object[] args, final Function function) { final Promise promise = new Promise(); - promise.setResolve(true); + promise.state_ = PromiseState.FULFILLED; if (args.length == 0) { promise.all_ = new Promise[0]; } @@ -201,38 +247,44 @@ final Promise promise = new Promise(window); final Promise thisPromise = this; - final PostponedAction thenAction = new PostponedAction(window.getDocument().getPage(), "Promise.then") { + thenAction_ = new PostponedAction(window.getDocument().getPage(), "Promise.then") { @Override public void execute() throws Exception { Context.enter(); try { - Object newValue = null; - final Function toExecute = isResolved(onRejected) ? onFulfilled : onRejected; - if (value_ instanceof Function) { - final WasCalledFunctionWrapper wrapper = new WasCalledFunctionWrapper(toExecute); + Function toExecute = null; + if (thisPromise.state_ == PromiseState.FULFILLED) { + toExecute = onFulfilled; + } + else if (thisPromise.state_ == PromiseState.REJECTED) { + toExecute = onRejected; + } + + if (toExecute != null) { try { - ((Function) value_).call(Context.getCurrentContext(), window, thisPromise, - new Object[] {wrapper, onRejected}); - if (wrapper.wasCalled_) { - newValue = wrapper.value_; + final Object newValue = toExecute.call(Context.getCurrentContext(), + window, thisPromise, new Object[] {value_}); + if (newValue instanceof Promise) { + final Promise callPromise = (Promise) newValue; + if (callPromise.state_ == PromiseState.FULFILLED) { + promise.update(PromiseState.FULFILLED, callPromise.value_); + } + else if (callPromise.state_ == PromiseState.REJECTED) { + promise.update(PromiseState.REJECTED, callPromise.value_); + } + else { + callPromise.linkedPromise_ = promise; + } } + else { + promise.update(PromiseState.FULFILLED, newValue); + } } catch (final JavaScriptException e) { - if (onRejected == null) { - promise.exceptionDetails_ = e.details(); - } - else if (!wrapper.wasCalled_) { - newValue = onRejected.call(Context.getCurrentContext(), window, thisPromise, - new Object[] {e.getValue()}); - } + promise.update(PromiseState.REJECTED, e.getValue()); } } - else { - newValue = toExecute.call(Context.getCurrentContext(), window, thisPromise, - new Object[] {value_}); - } - promise.value_ = newValue; } finally { Context.exit(); @@ -240,9 +292,15 @@ } }; - final JavaScriptEngine jsEngine - = (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine(); - jsEngine.addPostponedAction(thenAction); + if (state_ != PromiseState.PENDING) { + final JavaScriptEngine jsEngine + = (JavaScriptEngine) getWindow(this).getWebWindow().getWebClient().getJavaScriptEngine(); + jsEngine.addPostponedAction(thenAction_); + thenAction_ = null; + } + else { + thisPromise.linkedPromise_ = promise; + } return promise; } @@ -255,49 +313,6 @@ */ @JsxFunction(functionName = "catch") public Promise catch_js(final Function onRejected) { - final Window window = getWindow(); - final Promise promise = new Promise(window); - final Promise thisPromise = this; - - final PostponedAction thenAction = new PostponedAction(window.getDocument().getPage(), "Promise.catch") { - - @Override - public void execute() throws Exception { - Context.enter(); - try { - final Object newValue = onRejected.call(Context.getCurrentContext(), window, thisPromise, - new Object[] {exceptionDetails_}); - promise.value_ = newValue; - } - finally { - Context.exit(); - } - } - }; - - final JavaScriptEngine jsEngine - = (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine(); - jsEngine.addPostponedAction(thenAction); - - return promise; + return then(null, onRejected); } - - private static class WasCalledFunctionWrapper extends FunctionWrapper { - private boolean wasCalled_; - private Object value_; - - WasCalledFunctionWrapper(final Function wrapped) { - super(wrapped); - } - - /** - * {@inheritDoc} - */ - @Override - public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) { - wasCalled_ = true; - value_ = super.call(cx, scope, thisObj, args); - return value_; - } - } } Modified: trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/PromiseTest.java =================================================================== --- trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/PromiseTest.java 2017-10-04 10:13:15 UTC (rev 14857) +++ trunk/htmlunit/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/PromiseTest.java 2017-10-06 12:00:03 UTC (rev 14858) @@ -147,6 +147,45 @@ * @throws Exception if an error occurs */ @Test + @Alerts(DEFAULT = {"done", "false", "[object Promise]"}, + IE = {}) + public void rejectPromise() throws Exception { + final String html = + "<html>\n" + + "<head>\n" + + " <script>\n" + + " function test() {\n" + + " if (window.Promise) {\n" + + " var original = Promise.reject(42);\n" + + " var cast = Promise.reject(original);\n" + + " cast.then(function(v) {\n" + + " log('failure');\n" + + " }, function(value) {\n" + + " log(value);\n" + + " });\n" + + " log('done');\n" + + " log(original === cast);\n" + + " }\n" + + " }\n" + + " function log(x) {\n" + + " document.getElementById('log').value += x + '\\n';\n" + + " }\n" + + " </script>\n" + + "</head>\n" + + "<body onload='test()'>\n" + + " <textarea id='log' cols='80' rows='40'></textarea>\n" + + "</body>\n" + + "</html>"; + + final WebDriver driver = loadPage2(html); + final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", ""); + assertEquals(String.join("\n", getExpectedAlerts()), text); + } + + /** + * @throws Exception if an error occurs + */ + @Test @Alerts(DEFAULT = {"done", "undefined"}, IE = {}) public void resolveEmpty() throws Exception { @@ -219,8 +258,44 @@ * @throws Exception if an error occurs */ @Test - @Alerts(DEFAULT = {"done", "true"}, + @Alerts(DEFAULT = {"done", "HtmlUnit"}, IE = {}) + public void resolveString() throws Exception { + final String html = + "<html>\n" + + "<head>\n" + + " <script>\n" + + " function test() {\n" + + " if (window.Promise) {\n" + + " var p = Promise.resolve('HtmlUnit');\n" + + " p.then(function(v) {\n" + + " log(v);\n" + + " });\n" + + " log('done');\n" + + " }\n" + + " }\n" + + "\n" + + " function log(x) {\n" + + " document.getElementById('log').value += x + '\\n';\n" + + " }\n" + + " </script>\n" + + "</head>\n" + + "<body onload='test()'>\n" + + " <textarea id='log' cols='80' rows='40'></textarea>\n" + + "</body>\n" + + "</html>"; + + final WebDriver driver = loadPage2(html); + final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", ""); + assertEquals(String.join("\n", getExpectedAlerts()), text); + } + + /** + * @throws Exception if an error occurs + */ + @Test + @Alerts(DEFAULT = {"done", "true", "42"}, + IE = {}) public void resolvePromise() throws Exception { final String html = "<html>\n" @@ -228,12 +303,13 @@ + " <script>\n" + " function test() {\n" + " if (window.Promise) {\n" - + " var original = Promise.resolve(true);\n" + + " var original = Promise.resolve(42);\n" + " var cast = Promise.resolve(original);\n" + " cast.then(function(v) {\n" + " log(v);\n" + " });\n" + " log('done');\n" + + " log(original === cast);\n" + " }\n" + " }\n" + " function log(x) {\n" @@ -366,19 +442,35 @@ * @throws Exception if an error occurs */ @Test - @Alerts(DEFAULT = {"end in-then"}, - IE = "exception") + @Alerts(DEFAULT = {"done", "undefined"}, + IE = {}) public void then() throws Exception { - final String html = "<html><body><script>\n" - + "try {\n" - + " var p = Promise.resolve(void 0);\n" - + " p.then(function() { document.title += ' in-then'; });\n" - + " document.title += ' end';\n" - + "} catch (e) { document.title += ' exception'; }\n" - + "</script></body></html>"; - + final String html = "<html>\n" + + "<head>\n" + + " <script>\n" + + " function test() {\n" + + " if (window.Promise) {\n" + + " var p = Promise.resolve(void 0);\n" + + "\n" + + " p.then(function(value) {\n" + + " log(value);\n" + + " })\n" + + " log('done');\n" + + " }\n" + + " }\n" + + "\n" + + " function log(x) {\n" + + " document.getElementById('log').value += x + '\\n';\n" + + " }\n" + + " </script>\n" + + "</head>\n" + + "<body onload='test()'>\n" + + " <textarea id='log' cols='80' rows='40'></textarea>\n" + + "</body>\n" + + "</html>"; final WebDriver driver = loadPage2(html); - assertEquals(getExpectedAlerts()[0], driver.getTitle()); + final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", ""); + assertEquals(String.join("\n", getExpectedAlerts()), text); } /** @@ -393,11 +485,11 @@ + " <script>\n" + " function test() {\n" + " if (window.Promise) {\n" - + " var p1 = new Promise(function(resolve, reject) {\n" + + " var p = new Promise(function(resolve, reject) {\n" + " resolve('Success');\n" + " });\n" + "\n" - + " p1.then(function(value) {\n" + + " p.then(function(value) {\n" + " log(value);\n" + " throw 'oh, no!';\n" + " }).catch(function(e) {\n" @@ -506,4 +598,92 @@ final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", ""); assertEquals(String.join("\n", getExpectedAlerts()), text); } + + /** + * @throws Exception if an error occurs + */ + @Test + @Alerts(CHROME = { "function () { [native code] }", + "function () { [native code] }", + "[object Window]", + "done", "resolved value"}, + FF = { "function () {\n [native code]\n}", + "function () {\n [native code]\n}", + "[object Window]", + "done", "resolved value"}, + IE = {}) + public void constructor() throws Exception { + final String html = "<html>\n" + + "<head>\n" + + " <script>\n" + + " function test() {\n" + + " if (window.Promise) {\n" + + " var p = new Promise(function(resolve, reject) {\n" + + " log(resolve);\n" + + " log(reject);\n" + + " log(this);\n" + + " resolve('resolved value');\n" + + " });\n" + + " p.then(function(value) {log(value);});\n" + + " log('done');\n" + + " }\n" + + " }\n" + + "\n" + + " function log(x) {\n" + + " document.getElementById('log').value += x + '\\n';\n" + + " }\n" + + " </script>\n" + + "</head>\n" + + "<body onload='test()'>\n" + + " <textarea id='log' cols='80' rows='40'></textarea>\n" + + "</body>\n" + + "</html>"; + + final WebDriver driver = loadPage2(html); + final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", ""); + assertEquals(String.join("\n", getExpectedAlerts()), text); + } + + /** + * @throws Exception if an error occurs + */ + @Test + @Alerts(DEFAULT = {"true", "true", "true"}, + IE = {}) + public void constructorWithoutFunction() throws Exception { + final String html = "<html>\n" + + "<head>\n" + + " <script>\n" + + " function test() {\n" + + " if (window.Promise) {\n" + + " try{\n" + + " var p = new Promise();\n" + + " log('done');\n" + + " } catch(e) { log(e instanceof TypeError); }\n" + + " try{\n" + + " var p = new Promise([1, 2, 4]);\n" + + " log('done');\n" + + " } catch(e) { log(e instanceof TypeError); }\n" + + " try{\n" + + " var original = Promise.resolve(42);\n" + + " var p = new Promise(original);\n" + + " log('done');\n" + + " } catch(e) { log(e instanceof TypeError); }\n" + + " }\n" + + " }\n" + + "\n" + + " function log(x) {\n" + + " document.getElementById('log').value += x + '\\n';\n" + + " }\n" + + " </script>\n" + + "</head>\n" + + "<body onload='test()'>\n" + + " <textarea id='log' cols='80' rows='40'></textarea>\n" + + "</body>\n" + + "</html>"; + + final WebDriver driver = loadPage2(html); + final String text = driver.findElement(By.id("log")).getAttribute("value").trim().replaceAll("\r", ""); + assertEquals(String.join("\n", getExpectedAlerts()), text); + } } |