Skip to content

Commit bef892c

Browse files
[wptrunner] Reset permissions between tests for Chrome (#48610)
Similar to #48106, permission changes may leak between tests that use the same WebDriver session (i.e., browser process). For now, add a Chromium-specific `Browser.resetPermissions` call to reset permission defaults. To consolidate calls that reset state, refactor the WebDriver executor's testharness window management to align with #45735.
1 parent c7805fe commit bef892c

File tree

4 files changed

+53
-154
lines changed

4 files changed

+53
-154
lines changed

tools/wptrunner/wptrunner/executors/executorchrome.py

Lines changed: 37 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -80,66 +80,28 @@ class ChromeDriverTestharnessProtocolPart(WebDriverTestharnessProtocolPart):
8080
The main difference from the default WebDriver testharness implementation is
8181
that the test window can be reused between tests for better performance.
8282
"""
83-
84-
def setup(self):
85-
super().setup()
86-
# Handle (an alphanumeric string) that may be set if window reuse is
87-
# enabled. This state allows the protocol to distinguish the test
88-
# window from other windows a test itself may create that the "Get
89-
# Window Handles" command also returns.
90-
#
91-
# Because test window persistence is a Chrome-only feature, it's not
92-
# exposed to the base WebDriver testharness executor.
93-
self.test_window = None
94-
self.reuse_window = self.parent.reuse_window
95-
96-
def close_test_window(self):
97-
if self.test_window:
98-
self._close_window(self.test_window)
99-
self.test_window = None
100-
101-
def close_old_windows(self):
102-
self.webdriver.actions.release()
103-
for handle in self.webdriver.handles:
104-
if handle not in {self.runner_handle, self.test_window}:
105-
self._close_window(handle)
106-
if not self.reuse_window:
107-
self.close_test_window()
108-
self.webdriver.window_handle = self.runner_handle
109-
# TODO(web-platform-tests/wpt#48078): Find a cross-platform way to clear
83+
def reset_browser_state(self):
84+
# TODO(web-platform-tests/wpt#48078): Find a cross-vendor way to clear
11085
# cookies for all domains.
11186
self.parent.cdp.execute_cdp_command("Network.clearBrowserCookies")
112-
return self.runner_handle
113-
114-
def open_test_window(self, window_id):
115-
if self.test_window:
116-
# Try to reuse the existing test window by emulating the `about:blank`
117-
# page with no history you would get with a new window.
118-
try:
119-
self.webdriver.window_handle = self.test_window
120-
# Reset navigation history with Chrome DevTools Protocol:
121-
# https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-resetNavigationHistory
122-
self.parent.cdp.execute_cdp_command("Page.resetNavigationHistory")
123-
self.webdriver.url = "about:blank"
124-
return
125-
except error.NoSuchWindowException:
126-
self.test_window = None
127-
super().open_test_window(window_id)
128-
129-
def get_test_window(self, window_id, parent, timeout=5):
130-
if self.test_window:
131-
return self.test_window
132-
# Poll the handles endpoint for the test window like the base WebDriver
133-
# protocol part, but don't bother checking for the serialized
134-
# WindowProxy (not supported by Chrome currently).
135-
deadline = time.time() + timeout
136-
while time.time() < deadline:
137-
self.test_window = self._poll_handles_for_test_window(parent)
138-
if self.test_window is not None:
139-
assert self.test_window != parent
140-
return self.test_window
141-
time.sleep(0.03)
142-
raise Exception("unable to find test window")
87+
# Reset default permissions that `test_driver.set_permission(...)` may
88+
# have altered.
89+
self.parent.cdp.execute_cdp_command("Browser.resetPermissions")
90+
# Chromium requires the `background-sync` permission for reporting APIs
91+
# to work. Not all embedders (notably, `chrome --headless=old`) grant
92+
# `background-sync` by default, so this CDP call ensures the permission
93+
# is granted for all origins, in line with the background sync spec's
94+
# recommendation [0].
95+
#
96+
# WebDriver's "Set Permission" command can only act on the test's
97+
# origin, which may be too limited.
98+
#
99+
# [0]: https://wicg.github.io/background-sync/spec/#permission
100+
params = {
101+
"permission": {"name": "background-sync"},
102+
"setting": "granted",
103+
}
104+
self.parent.cdp.execute_cdp_command("Browser.setPermission", params)
143105

144106

145107
class ChromeDriverFedCMProtocolPart(WebDriverFedCMProtocolPart):
@@ -177,7 +139,6 @@ class ChromeDriverProtocol(WebDriverProtocol):
177139
if base_part.name not in {part.name for part in implements}:
178140
implements.append(base_part)
179141

180-
reuse_window = False
181142
# Prefix to apply to vendor-specific WebDriver extension commands.
182143
vendor_prefix = "goog"
183144

@@ -225,25 +186,24 @@ class ChromeDriverTestharnessExecutor(WebDriverTestharnessExecutor, _SanitizerMi
225186

226187
def __init__(self, *args, reuse_window=False, **kwargs):
227188
super().__init__(*args, **kwargs)
228-
self.protocol.reuse_window = reuse_window
189+
self.reuse_window = reuse_window
229190

230-
def setup(self, runner, protocol=None):
231-
super().setup(runner, protocol)
232-
# Chromium requires the `background-sync` permission for reporting APIs
233-
# to work. Not all embedders (notably, `chrome --headless=old`) grant
234-
# `background-sync` by default, so this CDP call ensures the permission
235-
# is granted for all origins, in line with the background sync spec's
236-
# recommendation [0].
237-
#
238-
# WebDriver's "Set Permission" command can only act on the test's
239-
# origin, which may be too limited.
240-
#
241-
# [0]: https://wicg.github.io/background-sync/spec/#permission
242-
params = {
243-
"permission": {"name": "background-sync"},
244-
"setting": "granted",
245-
}
246-
self.protocol.cdp.execute_cdp_command("Browser.setPermission", params)
191+
def get_or_create_test_window(self, protocol):
192+
test_window = self.protocol.testharness.persistent_test_window
193+
if test_window:
194+
try:
195+
# Mimic the "new window" WebDriver command by loading `about:blank`
196+
# with no other browsing history.
197+
protocol.base.set_window(test_window)
198+
protocol.base.load("about:blank")
199+
protocol.cdp.execute_cdp_command("Page.resetNavigationHistory")
200+
except error.NoSuchWindowException:
201+
test_window = self.protocol.testharness.persistent_test_window = None
202+
if not test_window:
203+
test_window = super().get_or_create_test_window(protocol)
204+
if self.reuse_window:
205+
self.protocol.testharness.persistent_test_window = test_window
206+
return test_window
247207

248208
def _get_next_message_classic(self, protocol, url, test_window):
249209
try:

tools/wptrunner/wptrunner/executors/executoredge.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ class EdgeDriverRefTestExecutor(WebDriverRefTestExecutor, _SanitizerMixin): # t
3030
class EdgeDriverTestharnessExecutor(WebDriverTestharnessExecutor, _SanitizerMixin): # type: ignore
3131
protocol_cls = EdgeDriverProtocol
3232

33-
def __init__(self, *args, reuse_window=False, **kwargs):
34-
super().__init__(*args, **kwargs)
35-
self.protocol.reuse_window = reuse_window
36-
3733

3834
class EdgeDriverPrintRefTestExecutor(EdgeDriverRefTestExecutor):
3935
protocol_cls = EdgeDriverProtocol

tools/wptrunner/wptrunner/executors/executorwebdriver.py

Lines changed: 15 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
import os
66
import socket
77
import threading
8-
import time
98
import traceback
10-
import uuid
119
from urllib.parse import urljoin
1210

1311
from .base import (AsyncCallbackHandler,
@@ -82,6 +80,9 @@ def set_timeout(self, timeout):
8280
body = {"type": "script", "ms": timeout * 1000}
8381
self.webdriver.send_session_command("POST", "timeouts", body)
8482

83+
def create_window(self, type="tab", **kwargs):
84+
return self.webdriver.new_window(type_hint=type)
85+
8586
@property
8687
def current_window(self):
8788
return self.webdriver.window_handle
@@ -226,10 +227,9 @@ class WebDriverTestharnessProtocolPart(TestharnessProtocolPart):
226227
def setup(self):
227228
self.webdriver = self.parent.webdriver
228229
self.runner_handle = None
230+
self.persistent_test_window = None
229231
with open(os.path.join(here, "runner.js")) as f:
230232
self.runner_script = f.read()
231-
with open(os.path.join(here, "window-loaded.js")) as f:
232-
self.window_loaded_script = f.read()
233233

234234
def load_runner(self, url_protocol):
235235
if self.runner_handle:
@@ -245,10 +245,11 @@ def load_runner(self, url_protocol):
245245

246246
def close_old_windows(self):
247247
self.webdriver.actions.release()
248-
handles = [item for item in self.webdriver.handles if item != self.runner_handle]
249-
for handle in handles:
250-
self._close_window(handle)
248+
for handle in self.webdriver.handles:
249+
if handle not in {self.runner_handle, self.persistent_test_window}:
250+
self._close_window(handle)
251251
self.webdriver.window_handle = self.runner_handle
252+
self.reset_browser_state()
252253
return self.runner_handle
253254

254255
def _close_window(self, window_handle):
@@ -258,65 +259,8 @@ def _close_window(self, window_handle):
258259
except webdriver_error.NoSuchWindowException:
259260
pass
260261

261-
def open_test_window(self, window_id):
262-
self.webdriver.execute_script(
263-
"window.open('about:blank', '%s', 'noopener')" % window_id)
264-
265-
def get_test_window(self, window_id, parent, timeout=5):
266-
"""Find the test window amongst all the open windows.
267-
This is assumed to be either the named window or the one after the parent in the list of
268-
window handles
269-
270-
:param window_id: The DOM name of the Window
271-
:param parent: The handle of the runner window
272-
:param timeout: The time in seconds to wait for the window to appear. This is because in
273-
some implementations there's a race between calling window.open and the
274-
window being added to the list of WebDriver accessible windows."""
275-
test_window = None
276-
end_time = time.time() + timeout
277-
while time.time() < end_time:
278-
try:
279-
# Try using the JSON serialization of the WindowProxy object,
280-
# it's in Level 1 but nothing supports it yet
281-
win_s = self.webdriver.execute_script("return window['%s'];" % window_id)
282-
win_obj = json.loads(win_s)
283-
test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
284-
except Exception:
285-
pass
286-
287-
if test_window is None:
288-
test_window = self._poll_handles_for_test_window(parent)
289-
290-
if test_window is not None:
291-
assert test_window != parent
292-
return test_window
293-
294-
time.sleep(0.1)
295-
296-
raise Exception("unable to find test window")
297-
298-
def _poll_handles_for_test_window(self, parent):
299-
test_window = None
300-
after = self.webdriver.handles
301-
if len(after) == 2:
302-
test_window = next(iter(set(after) - {parent}))
303-
elif after[0] == parent and len(after) > 2:
304-
# Hope the first one here is the test window
305-
test_window = after[1]
306-
return test_window
307-
308-
def test_window_loaded(self):
309-
"""Wait until the page in the new window has been loaded.
310-
311-
Hereby ignore Javascript execptions that are thrown when
312-
the document has been unloaded due to a process change.
313-
"""
314-
while True:
315-
try:
316-
self.webdriver.execute_script(self.window_loaded_script, asynchronous=True)
317-
break
318-
except webdriver_error.JavascriptErrorException:
319-
pass
262+
def reset_browser_state(self):
263+
"""Reset browser-wide state that normally persists between tests."""
320264

321265

322266
class WebDriverPrintProtocolPart(PrintProtocolPart):
@@ -828,7 +772,6 @@ def __init__(self, logger, browser, server_config, timeout_multiplier=1,
828772
self._get_next_message = self._get_next_message_classic
829773

830774
self.close_after_done = close_after_done
831-
self.window_id = str(uuid.uuid4())
832775
self.cleanup_after_test = cleanup_after_test
833776

834777
def is_alive(self):
@@ -857,20 +800,16 @@ def do_test(self, test):
857800
def do_testharness(self, protocol, url, timeout):
858801
# The previous test may not have closed its old windows (if something
859802
# went wrong or if cleanup_after_test was False), so clean up here.
860-
parent_window = protocol.testharness.close_old_windows()
803+
protocol.testharness.close_old_windows()
861804

862805
# If protocol implements `bidi_events`, remove all the existing subscriptions.
863806
if hasattr(protocol, 'bidi_events'):
864807
# Use protocol loop to run the async cleanup.
865808
protocol.loop.run_until_complete(protocol.bidi_events.unsubscribe_all())
866809

867810
# Now start the test harness
868-
protocol.testharness.open_test_window(self.window_id)
869-
test_window = protocol.testharness.get_test_window(self.window_id,
870-
parent_window,
871-
timeout=5*self.timeout_multiplier)
811+
test_window = self.get_or_create_test_window(protocol)
872812
self.protocol.base.set_window(test_window)
873-
874813
# Wait until about:blank has been loaded
875814
protocol.base.execute_script(self.window_loaded_script, asynchronous=True)
876815

@@ -975,6 +914,9 @@ async def process_bidi_event(method, params):
975914

976915
return rv, extra
977916

917+
def get_or_create_test_window(self, protocol):
918+
return protocol.base.create_window()
919+
978920
def _get_next_message_classic(self, protocol, url, _):
979921
"""
980922
Get the next message from the test_driver using the classic WebDriver async script execution. This will block

tools/wptrunner/wptrunner/executors/protocol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ def close_old_windows(self, url_protocol):
215215
contains the initial runner page.
216216
217217
:param str url_protocol: "https" or "http" depending on the test metadata.
218+
:returns: A browser-specific handle to the runner page.
218219
"""
219220
pass
220221

0 commit comments

Comments
 (0)