wip draft
authorAndreas Tolfsen <ato@sny.no>
Thu, 27 Jul 2017 01:12:52 +0100
changeset 618578 e6538f672065b4b04983837a988bb10690d3fbfa
parent 618577 5531cc68a0df780f4d6e192aa138358214855457
child 640117 35a9afb7783f8fd066f24a2c6d5523a6a97230b5
push id71384
push userbmo:ato@sny.no
push dateMon, 31 Jul 2017 18:57:50 +0000
milestone56.0a1
wip MozReview-Commit-ID: EpWJNFGvu86
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py
testing/marionette/wait.js
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/tools/webdriver/webdriver/client.py
testing/web-platform/tests/webdriver/tests/maximize_window.py
testing/web-platform/tests/webdriver/tests/support/asserts.py
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -43,17 +43,17 @@ Cu.import("chrome://marionette/content/e
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/l10n.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/reftest.js");
 Cu.import("chrome://marionette/content/session.js");
-Cu.import("chrome://marionette/content/wait.js");
+const {wait, TimedPromise} = Cu.import("chrome://marionette/content/wait.js", {});
 
 Cu.importGlobalProperties(["URL"]);
 
 this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
 
 var FRAME_SCRIPT = "chrome://marionette/content/listener.js";
 
 const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
@@ -2944,30 +2944,105 @@ GeckoDriver.prototype.minimizeWindow = f
  *
  * @throws {UnsupportedOperationError}
  *     Not available for current application.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
-GeckoDriver.prototype.maximizeWindow = function* (cmd, resp) {
+GeckoDriver.prototype.maximizeWindow = async function(cmd, resp) {
   assert.firefox();
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  yield new Promise(resolve => {
-    win.addEventListener("resize", resolve, {once: true});
-
-    if (win.windowState == win.STATE_MAXIMIZED) {
+  const {STATE_MAXIMIZED, STATE_NORMAL} = win;
+
+  let origSize = {
+    outerWidth: win.outerWidth,
+    outerHeight: win.outerHeight,
+  };
+
+  dump("before width=" + origSize.outerWidth + "\n");
+  dump("before height=" + origSize.outerHeight + "\n");
+
+  let expectedWindowState;
+  if (win.windowState == STATE_MAXIMIZED) {
+    expectedWindowState = STATE_NORMAL;
+  } else {
+    expectedWindowState = STATE_MAXIMIZED;
+  }
+  dump("expectedWindowState=" + expectedWindowState + "\n");
+
+  // Poll-wait for ChromeWindow.windowState to reach |state|.
+  async function windowState(state) {
+    return wait.until((resolve, reject) => {
+      dump("win.windowState=" + win.windowState + "\n");
+      if (win.windowState == state) {
+        dump("resolve\n");
+        resolve();
+      } else {
+        dump("reject\n");
+        reject();
+      }
+    });
+  }
+
+  // Wait for the window size to change |from|.
+  async function windowSizeChange(from) {
+    await wait.until((resolve, reject) => {
+      let curSize = {
+        outerWidth: win.outerWidth,
+        outerHeight: win.outerHeight,
+      };
+      dump("curSize=" + JSON.stringify(curSize) + "\n");
+      if (curSize.outerWidth != origSize.outerWidth ||
+          curSize.outerHeight != origSize.outerHeight) {
+        dump("  resolve\n");
+        resolve();
+      } else {
+        dump("  reject\n");
+        reject();
+      }
+    });
+  }
+
+  let modeChangeEv;
+  await new TimedPromise(resolve => {
+    dump("inside the timedpromise\n");
+
+    modeChangeEv = ev => {
+      dump("====> sizemodechange\n");
+      dump("  width=" + win.outerWidth + "\n");
+      dump("  height=" + win.outerHeight + "\n");
+
+      resolve();
+    };
+    win.addEventListener("sizemodechange", modeChangeEv, {once: true});
+
+    if (win.windowState == STATE_MAXIMIZED) {
+      dump("===> restoring\n");
       win.restore();
     } else {
+      dump("===> maximizing\n");
       win.maximize();
     }
-  });
+
+    dump("returning from timedpromise\n");
+  }, {throws: null});
+  win.removeEventListener("sizemodechange", modeChangeEv);
+
+  dump("awaiting windowState to reach " + expectedWindowState + "\n");
+  await windowState(expectedWindowState);
+
+  dump("awaiting window size to change\n");
+  await windowSizeChange();
+
+  dump("after width=" + win.outerWidth + "\n");
+  dump("after height=" + win.outerHeight + "\n");
 
   resp.body = {
     x: win.screenX,
     y: win.screenY,
     width: win.outerWidth,
     height: win.outerHeight,
   };
 };
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py
@@ -1,14 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-from marionette_driver.errors import InvalidArgumentException
-
 from marionette_harness import MarionetteTestCase
 
 
 class TestWindowMaximize(MarionetteTestCase):
 
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.max = self.marionette.execute_script("""
@@ -34,23 +32,23 @@ class TestWindowMaximize(MarionetteTestC
         if self.marionette.session_capabilities["platformName"] == "windows_nt":
             delta = 16
         else:
             delta = 8
 
         self.assertGreaterEqual(
             actual["width"], self.max["width"] - delta,
             msg="Window width is not within {delta} px of availWidth: "
-                "current width {expected} should be greater than max width {max}"
-                .format(delta=delta, expected=actual["width"], max=self.max["width"] - delta))
+                "current width {current} should be greater than or equal to max width {max}"
+                .format(delta=delta, current=actual["width"], max=self.max["width"] - delta))
         self.assertGreaterEqual(
-            actual["height"], self.max["height"],
+            actual["height"], self.max["height"] - delta,
             msg="Window height is not within {delta} px of availHeight: "
-                "current height {expected} should be greater than max width {max}"
-                .format(delta=delta, expected=actual["height"], max=self.max["height"] - delta))
+                "current height {current} should be greater than or equal to max height {max}"
+                .format(delta=delta, current=actual["height"], max=self.max["height"] - delta))
 
     def assert_window_restored(self, actual):
         self.assertEqual(self.original_size["width"], actual["width"])
         self.assertEqual(self.original_size["height"], actual["height"])
 
     def assert_window_rect(self, rect):
         self.assertIn("width", rect)
         self.assertIn("height", rect)
--- a/testing/marionette/wait.js
+++ b/testing/marionette/wait.js
@@ -1,29 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.import("chrome://marionette/content/error.js");
+const {
+  error,
+  TimeoutError,
+} = Cu.import("chrome://marionette/content/error.js", {});
 
-this.EXPORTED_SYMBOLS = ["wait"];
+this.EXPORTED_SYMBOLS = ["wait", "TimedPromise"];
 
 /**
  * Poll-waiting utilities.
  *
  * @namespace
  */
 this.wait = {};
 
+const {TYPE_ONE_SHOT, TYPE_REPEATING_SLACK} = Ci.nsITimer;
+
 /**
- * @callback WaitCondition
+ * @callback Condition
  *
  * @param {function(*)} resolve
  *     To be called when the condition has been met.  Will return the
  *     resolved value.
  * @param {function} reject
  *     To be called when the condition has not been met.  Will cause
  *     the condition to be revaluated or time out.
  *
@@ -57,17 +62,17 @@ this.wait = {};
  *       if (res.length > 0) {
  *         resolve(Array.from(res));
  *       } else {
  *         reject([]);
  *       }
  *     });
  * </pre></code>
  *
- * @param {WaitCondition} func
+ * @param {Condition} func
  *     Function to run off the main thread.
  * @param {number=} timeout
  *     Desired timeout.  If 0 or less than the runtime evaluation time
  *     of |func|, |func| is guaranteed to run at least once.  The default
  *     is 2000 milliseconds.
  * @param {number=} interval
  *     Duration between each poll of |func| in milliseconds.  Defaults to
  *     10 milliseconds.
@@ -99,19 +104,79 @@ wait.until = function(func, timeout = 20
         }
       }).catch(reject);
     };
 
     // the repeating slack timer waits |interval|
     // before invoking |evalFn|
     evalFn();
 
-    timer.init(evalFn, interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
+    timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
 
-  // cancel timer and propagate result
   }).then(res => {
     timer.cancel();
     return res;
   }, err => {
     timer.cancel();
     throw err;
   });
 };
+
+/**
+ * The <code>TimedPromise</code> object represents the timed, eventual
+ * completion (or failure) of an asynchronous operation, and its
+ * resulting value.
+ *
+ * In contrast to a regular {@link Promise}, it times out after
+ * <var>timeout</var>.
+ *
+ * @param {Condition} func
+ *     Function to run, which will have its <code>reject</code>
+ *     callback invoked after the <var>timeout</var> duration is reached.
+ *     It is presented with two callbacks: <code>resolve(value)</code>
+ *     and <code>reject(error)</code>.
+ * @param {timeout=} [timeout=2000] timeout
+ *     <var>condition</var>'s <code>reject</code> callback will be called
+ *     after this timeout.
+ * @param {Error=} [throws=TimeoutError] throws
+ *     When the <var>timeout</var> is hit, this error class will be
+ *     thrown.  If it is null, no error is thrown and the promise is
+ *     instead resolved.
+ *
+ * @return {Promise.<*>}
+ *     Timed promise.
+ */
+function TimedPromise(fn, {timeout = 1500, throws = TimeoutError} = {}) {
+  const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+  return new Promise((resolve, reject) => {
+    // Reject only if |errorCls| is given.  Otherwise, the user is OK
+    // with the promise timing out.
+    let bail = res => {
+      dump("  timed promise timed out!\n");
+      if (throws !== null) {
+        let err = new throws();
+        reject(err);
+      } else {
+        resolve(res);
+      }
+    };
+
+    timer.initWithCallback({notify: bail}, timeout, TYPE_ONE_SHOT);
+
+    try {
+      dump("  calling fn...\n");
+      fn(resolve, reject);
+      dump("  ...fn returned\n");
+    } catch (e) {
+      reject(e);
+    }
+
+  }).then(res => {
+    dump("  resolve res=" + res + "\n");
+    timer.cancel();
+    return res;
+  }, err => {
+    dump("  reject err=" + err + "\n");
+    timer.cancel();
+    throw err;
+  });
+}
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -402860,27 +402860,27 @@
     ]
    ],
    "webdriver/tests/cookies.py": [
     [
      "/webdriver/tests/cookies.py",
      {}
     ]
    ],
+   "webdriver/tests/maximize_window.py": [
+    [
+     "/webdriver/tests/maximize_window.py",
+     {}
+    ]
+   ],
    "webdriver/tests/navigation.py": [
     [
      "/webdriver/tests/navigation.py",
      {}
     ]
-   ],
-   "webdriver/tests/window_maximizing.py": [
-    [
-     "/webdriver/tests/window_maximizing.py",
-     {}
-    ]
    ]
   }
  },
  "paths": {
   "./.codecov.yml": [
    "e2322808739a5977e90896b4755cfc20f4ab2046",
    "support"
   ],
@@ -621651,26 +621651,30 @@
   "webdriver/tests/contexts.py": [
    "9c4be1b08b99945621b149d1aa2aa64167caad50",
    "wdspec"
   ],
   "webdriver/tests/cookies.py": [
    "e31177e638269864031e44808945fa1e7c46031c",
    "wdspec"
   ],
+  "webdriver/tests/maximize_window.py": [
+   "827b3a3e1a3ef628dae1480af029fc01ef5e9388",
+   "wdspec"
+  ],
   "webdriver/tests/navigation.py": [
    "cec2987258d9c807a247da9e0216b3af1f171484",
    "wdspec"
   ],
   "webdriver/tests/support/__init__.py": [
    "5a31a3917a5157516c10951a3b3d5ffb43b992d9",
    "support"
   ],
   "webdriver/tests/support/asserts.py": [
-   "cf1d298a9dc61b07eb9efe1ff3ed98a318d48bc4",
+   "693c848df03b46e7b4f5e17e49fd0879a29a0d1c",
    "support"
   ],
   "webdriver/tests/support/fixtures.py": [
    "6ceec11f42cd9be53a92ad88aa07657c78779ce3",
    "support"
   ],
   "webdriver/tests/support/http_request.py": [
    "01c4b525c27f77d253c75031a9cee3f17aca8df0",
@@ -621683,20 +621687,16 @@
   "webdriver/tests/support/merge_dictionaries.py": [
    "84a6d3c6f8f4afded0f21264bbaeebec38a7f827",
    "support"
   ],
   "webdriver/tests/support/wait.py": [
    "a4b0c9c340ea7055139d9fcab3246ee836d6a441",
    "support"
   ],
-  "webdriver/tests/window_maximizing.py": [
-   "ba6b9109f5baaf6eb300a3f89f984753e9d5adb9",
-   "wdspec"
-  ],
   "webgl/OWNERS": [
    "f8e0703fe2cc88edd21ef2c94fcb2e1a8889f5ae",
    "support"
   ],
   "webgl/bufferSubData.html": [
    "526612470551a0eb157b310c587d50080087808d",
    "testharness"
   ],
--- a/testing/web-platform/tests/tools/webdriver/webdriver/client.py
+++ b/testing/web-platform/tests/tools/webdriver/webdriver/client.py
@@ -258,17 +258,16 @@ class Window(object):
 
     @position.setter
     @command
     def position(self, data):
         data = x, y
         body = {"x": x, "y": y}
         self.session.send_session_command("POST", "window/rect", body)
 
-    @property
     @command
     def maximize(self):
         return self.session.send_session_command("POST", "window/maximize")
 
 
 class Find(object):
     def __init__(self, session):
         self.session = session
@@ -385,20 +384,16 @@ class Session(object):
     def end(self):
         if self.session_id is None:
             return
 
         url = "session/%s" % self.session_id
         self.send_command("DELETE", url)
 
         self.session_id = None
-        self.timeouts = None
-        self.window = None
-        self.find = None
-        self.extension = None
 
     def send_command(self, method, url, body=None):
         """
         Send a command to the remote end and validate its success.
 
         :param method: HTTP method to use in request.
         :param uri: "Command part" of the HTTP request URL,
             e.g. `window/rect`.
--- a/testing/web-platform/tests/webdriver/tests/maximize_window.py
+++ b/testing/web-platform/tests/webdriver/tests/maximize_window.py
@@ -4,35 +4,72 @@ from tests.support.asserts import assert
 alert_doc = inline("<script>window.alert()</script>")
 
 # 10.7.3 Maximize Window
 def test_maximize_no_browsing_context(session, create_window):
     # Step 1
     session.window_handle = create_window()
     session.close()
     result = session.transport.send("POST", "session/%s/window/maximize" % session.session_id)
-
     assert_error(result, "no such window")
 
 
-def test_maximize_rect_alert_prompt(session):
+def test_handle_user_prompt(session):
     # Step 2
     session.url = alert_doc
-
     result = session.transport.send("POST", "session/%s/window/maximize" % session.session_id)
-
     assert_error(result, "unexpected alert open")
 
 
-def test_maximize_payload(session):
-    # step 5
+def test_maximize(session):
+    before = session.window.size
+
+    # step 4
+    result = session.transport.send("POST", "session/%s/window/maximize" % session.session_id)
+    assert_success(result)
+
+    after = session.window.size
+    assert before != after
+
+
+def test_payload(session):
+    before = session.window.size
+
     result = session.transport.send("POST", "session/%s/window/maximize" % session.session_id)
 
+    # step 5
     assert result.status == 200
     assert isinstance(result.body["value"], dict)
-    assert "width" in result.body["value"]
-    assert "height" in result.body["value"]
-    assert "x" in result.body["value"]
-    assert "y" in result.body["value"]
-    assert isinstance(result.body["value"]["width"], float)
-    assert isinstance(result.body["value"]["height"], float)
-    assert isinstance(result.body["value"]["x"], float)
-    assert isinstance(result.body["value"]["y"], float)
+
+    rect = result.body["value"]
+    assert "width" in rect
+    assert "height" in rect
+    assert "x" in rect
+    assert "y" in rect
+    assert isinstance(rect["width"], float)
+    assert isinstance(rect["height"], float)
+    assert isinstance(rect["x"], float)
+    assert isinstance(rect["y"], float)
+
+    after = session.window.size
+    assert before != after
+
+
+def test_maximize_when_resized_to_max_size(session):
+    # Determine the largest available window size by first maximising
+    # the window and getting the window rect dimensions.
+    #
+    # Then resize the window to the maximum available size.
+    session.end()
+    available = session.window.maximize()
+    session.end()
+
+    session.window.size = (int(available["width"]), int(available["height"]))
+
+    # In certain window managers a window extending to the full available
+    # dimensions of the screen may not imply that the window is maximised,
+    # since this is often a special state.  If a remote end expects a DOM
+    # resize event, this may not fire if the window has already reached
+    # its expected dimensions.
+    before = session.window.size
+    session.window.maximize()
+    after = session.window.size
+    assert before == after
--- a/testing/web-platform/tests/webdriver/tests/support/asserts.py
+++ b/testing/web-platform/tests/webdriver/tests/support/asserts.py
@@ -63,26 +63,29 @@ def assert_error(response, error_code):
     :param error_code: string value of the expected "error code"
     """
     assert response.status == errors[error_code]
     assert "value" in response.body
     assert response.body["value"]["error"] == error_code
     assert isinstance(response.body["value"]["message"], basestring)
     assert isinstance(response.body["value"]["stacktrace"], basestring)
 
-def assert_success(response, value):
+def assert_success(response, value=None):
     """Verify that the provided wdclient.Response instance described a valid
     error response as defined by `dfn-send-an-error` and the provided error
     code.
-    :param response: wdclient.Response instance
-    :param value: expected value of the response body
+
+    :param response: wdclient.Response instance.
+    :param value: Expected value of the response body, if any.
+
     """
-
     assert response.status == 200
-    assert response.body["value"] == value
+    assert "value" in response.body
+    if value is not None:
+        assert response.body["value"] == value
 
 def assert_dialog_handled(session, expected_text):
     result = session.transport.send("GET",
                                     "session/%s/alert/text" % session.session_id)
 
     # If there were any existing dialogs prior to the creation of this
     # fixture's dialog, then the "Get Alert Text" command will return
     # successfully. In that case, the text must be different than that