Skip to content

Commit e9e5b1d

Browse files
committed
follow-up
1 parent bb5fc3e commit e9e5b1d

File tree

9 files changed

+219
-22
lines changed

9 files changed

+219
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
44

55
| | Linux | macOS | Windows |
66
| :--- | :---: | :---: | :---: |
7-
| Chromium <!-- GEN:chromium-version -->125.0.6422.14<!-- GEN:stop --> ||||
7+
| Chromium <!-- GEN:chromium-version -->125.0.6422.26<!-- GEN:stop --> ||||
88
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> ||||
99
| Firefox <!-- GEN:firefox-version -->125.0.1<!-- GEN:stop --> ||||
1010

playwright/_impl/_json_pipe.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import asyncio
16-
from typing import Dict, cast
16+
from typing import Dict, Optional, cast
1717

1818
from pyee.asyncio import AsyncIOEventEmitter
1919

@@ -54,9 +54,10 @@ def handle_message(message: Dict) -> None:
5454
return
5555
self.on_message(cast(ParsedMessagePayload, message))
5656

57-
def handle_closed(reason: str) -> None:
57+
def handle_closed(reason: Optional[str]) -> None:
5858
self.emit("close", reason)
59-
self.on_error_future.set_exception(TargetClosedError(reason))
59+
if reason:
60+
self.on_error_future.set_exception(TargetClosedError(reason))
6061
self._stopped_future.set_result(None)
6162

6263
self._pipe_channel.on(
@@ -65,7 +66,7 @@ def handle_closed(reason: str) -> None:
6566
)
6667
self._pipe_channel.on(
6768
"closed",
68-
lambda params: handle_closed(params["reason"]),
69+
lambda params: handle_closed(params.get("reason")),
6970
)
7071

7172
async def run(self) -> None:

playwright/_impl/_page.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1292,7 +1292,7 @@ async def set_checked(
12921292
async def add_locator_handler(
12931293
self,
12941294
locator: "Locator",
1295-
handler: Callable,
1295+
handler: Union[Callable[["Locator"], Any], Callable[[], Any]],
12961296
noWaitAfter: bool = None,
12971297
times: int = None,
12981298
) -> None:

playwright/async_api/_generated.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11723,7 +11723,9 @@ async def set_checked(
1172311723
async def add_locator_handler(
1172411724
self,
1172511725
locator: "Locator",
11726-
handler: typing.Callable,
11726+
handler: typing.Union[
11727+
typing.Callable[["Locator"], typing.Any], typing.Callable[[], typing.Any]
11728+
],
1172711729
*,
1172811730
no_wait_after: typing.Optional[bool] = None,
1172911731
times: typing.Optional[int] = None
@@ -11818,7 +11820,7 @@ def handler(locator):
1181811820
----------
1181911821
locator : Locator
1182011822
Locator that triggers the handler.
11821-
handler : Callable
11823+
handler : Union[Callable[[Locator], Any], Callable[[], Any]]
1182211824
Function that should be run once `locator` appears. This function should get rid of the element that blocks actions
1182311825
like click.
1182411826
no_wait_after : Union[bool, None]

playwright/sync_api/_generated.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11808,7 +11808,9 @@ def set_checked(
1180811808
def add_locator_handler(
1180911809
self,
1181011810
locator: "Locator",
11811-
handler: typing.Callable,
11811+
handler: typing.Union[
11812+
typing.Callable[["Locator"], typing.Any], typing.Callable[[], typing.Any]
11813+
],
1181211814
*,
1181311815
no_wait_after: typing.Optional[bool] = None,
1181411816
times: typing.Optional[int] = None
@@ -11903,7 +11905,7 @@ def handler(locator):
1190311905
----------
1190411906
locator : Locator
1190511907
Locator that triggers the handler.
11906-
handler : Callable
11908+
handler : Union[Callable[[Locator], Any], Callable[[], Any]]
1190711909
Function that should be run once `locator` appears. This function should get rid of the element that blocks actions
1190811910
like click.
1190911911
no_wait_after : Union[bool, None]

scripts/expected_api_mismatch.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ Parameter type mismatch in BrowserContext.unroute(handler=): documented as Union
1313
Parameter type mismatch in Page.route(handler=): documented as Callable[[Route, Request], Union[Any, Any]], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any]]
1414
Parameter type mismatch in Page.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None]
1515

16-
Parameter type mismatch in Page.add_locator_handler(handler=): documented as Callable[[Locator], Any], code has Callable
16+
# One vs two arguments in the callback, Python explicitly unions.
17+
Parameter type mismatch in Page.add_locator_handler(handler=): documented as Callable[[Locator], Any], code has Union[Callable[[Locator], Any], Callable[[], Any]]

tests/async/test_browsertype_connect_cdp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ async def test_conect_over_a_ws_endpoint(
9292
async def test_connect_over_cdp_passing_header_works(
9393
browser_type: BrowserType, server: Server
9494
) -> None:
95+
server.send_on_web_socket_connection(b"incoming")
9596
request = asyncio.create_task(server.wait_for_request("/ws"))
9697
with pytest.raises(Error):
9798
await browser_type.connect_over_cdp(

tests/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,17 @@ def process(self) -> None:
8686
uri = urlparse(self.uri.decode())
8787
path = uri.path
8888

89-
if path == "/ws":
90-
server._ws_resource.render(self)
91-
return
92-
9389
request_subscriber = server.request_subscribers.get(path)
9490
if request_subscriber:
9591
request_subscriber._loop.call_soon_threadsafe(
9692
request_subscriber.set_result, self
9793
)
9894
server.request_subscribers.pop(path)
9995

96+
if path == "/ws":
97+
server._ws_resource.render(self)
98+
return
99+
100100
if server.auth.get(path):
101101
authorization_header = self.requestHeaders.getRawHeaders("authorization")
102102
creds_correct = False

tests/sync/test_page_add_locator_handler.py

Lines changed: 197 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
1516
import pytest
1617

17-
from playwright.sync_api import Error, Page, expect
18+
from playwright.sync_api import Error, Locator, Page, expect
1819
from tests.server import Server
1920
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
2021

@@ -25,16 +26,18 @@ def test_should_work(page: Page, server: Server) -> None:
2526
before_count = 0
2627
after_count = 0
2728

28-
def handler() -> None:
29+
original_locator = page.get_by_text("This interstitial covers the button")
30+
31+
def handler(locator: Locator) -> None:
32+
nonlocal original_locator
33+
assert locator == original_locator
2934
nonlocal before_count
3035
nonlocal after_count
3136
before_count += 1
3237
page.locator("#close").click()
3338
after_count += 1
3439

35-
page.add_locator_handler(
36-
page.locator("text=This interstitial covers the button"), handler
37-
)
40+
page.add_locator_handler(original_locator, handler)
3841

3942
for args in [
4043
["mouseover", 1],
@@ -70,7 +73,7 @@ def handler() -> None:
7073
if page.get_by_text("This interstitial covers the button").is_visible():
7174
page.locator("#close").click()
7275

73-
page.add_locator_handler(page.locator("body"), handler)
76+
page.add_locator_handler(page.locator("body"), handler, no_wait_after=True)
7477

7578
for args in [
7679
["mouseover", 2],
@@ -152,7 +155,7 @@ def handler() -> None:
152155
# Deliberately timeout.
153156
try:
154157
page.wait_for_timeout(9999999)
155-
except Error:
158+
except Exception:
156159
pass
157160

158161
page.add_locator_handler(
@@ -195,3 +198,190 @@ def handler() -> None:
195198
expect(page.locator("#target")).to_be_visible()
196199
expect(page.locator("#interstitial")).not_to_be_visible()
197200
assert called == 1
201+
202+
203+
def test_should_work_when_owner_frame_detaches(page: Page, server: Server) -> None:
204+
page.goto(server.EMPTY_PAGE)
205+
page.evaluate(
206+
"""
207+
() => {
208+
const iframe = document.createElement('iframe');
209+
iframe.src = 'data:text/html,<body>hello from iframe</body>';
210+
document.body.append(iframe);
211+
212+
const target = document.createElement('button');
213+
target.textContent = 'Click me';
214+
target.id = 'target';
215+
target.addEventListener('click', () => window._clicked = true);
216+
document.body.appendChild(target);
217+
218+
const closeButton = document.createElement('button');
219+
closeButton.textContent = 'close';
220+
closeButton.id = 'close';
221+
closeButton.addEventListener('click', () => iframe.remove());
222+
document.body.appendChild(closeButton);
223+
}
224+
"""
225+
)
226+
page.add_locator_handler(
227+
page.frame_locator("iframe").locator("body"),
228+
lambda: page.locator("#close").click(),
229+
)
230+
page.locator("#target").click()
231+
assert page.query_selector("iframe") is None
232+
assert page.evaluate("window._clicked") is True
233+
234+
235+
def test_should_work_with_times_option(page: Page, server: Server) -> None:
236+
page.goto(server.PREFIX + "/input/handle-locator.html")
237+
called = 0
238+
239+
def _handler() -> None:
240+
nonlocal called
241+
called += 1
242+
243+
page.add_locator_handler(
244+
page.locator("body"), _handler, no_wait_after=True, times=2
245+
)
246+
page.locator("#aside").hover()
247+
page.evaluate(
248+
"""
249+
() => {
250+
window.clicked = 0;
251+
window.setupAnnoyingInterstitial('mouseover', 4);
252+
}
253+
"""
254+
)
255+
with pytest.raises(Error) as exc_info:
256+
page.locator("#target").click(timeout=3000)
257+
assert called == 2
258+
assert page.evaluate("window.clicked") == 0
259+
expect(page.locator("#interstitial")).to_be_visible()
260+
assert "Timeout 3000ms exceeded" in exc_info.value.message
261+
assert (
262+
'<div>This interstitial covers the button</div> from <div class="visible" id="interstitial">…</div> subtree intercepts pointer events'
263+
in exc_info.value.message
264+
)
265+
266+
267+
def test_should_wait_for_hidden_by_default(page: Page, server: Server) -> None:
268+
page.goto(server.PREFIX + "/input/handle-locator.html")
269+
called = 0
270+
271+
def _handler(button: Locator) -> None:
272+
nonlocal called
273+
called += 1
274+
button.click()
275+
276+
page.add_locator_handler(page.get_by_role("button", name="close"), _handler)
277+
page.locator("#aside").hover()
278+
page.evaluate(
279+
"""
280+
() => {
281+
window.clicked = 0;
282+
window.setupAnnoyingInterstitial('timeout', 1);
283+
}
284+
"""
285+
)
286+
page.locator("#target").click()
287+
assert page.evaluate("window.clicked") == 1
288+
expect(page.locator("#interstitial")).not_to_be_visible()
289+
assert called == 1
290+
291+
292+
def test_should_wait_for_hidden_by_default_2(page: Page, server: Server) -> None:
293+
page.goto(server.PREFIX + "/input/handle-locator.html")
294+
called = 0
295+
296+
def _handler() -> None:
297+
nonlocal called
298+
called += 1
299+
300+
page.add_locator_handler(page.get_by_role("button", name="close"), _handler)
301+
page.locator("#aside").hover()
302+
page.evaluate(
303+
"""
304+
() => {
305+
window.clicked = 0;
306+
window.setupAnnoyingInterstitial('hide', 1);
307+
}
308+
"""
309+
)
310+
with pytest.raises(Error) as exc_info:
311+
page.locator("#target").click(timeout=3000)
312+
assert page.evaluate("window.clicked") == 0
313+
expect(page.locator("#interstitial")).to_be_visible()
314+
assert called == 1
315+
assert (
316+
'locator handler has finished, waiting for get_by_role("button", name="close") to be hidden'
317+
in exc_info.value.message
318+
)
319+
320+
321+
def test_should_work_with_noWaitAfter(page: Page, server: Server) -> None:
322+
page.goto(server.PREFIX + "/input/handle-locator.html")
323+
called = 0
324+
325+
def _handler(button: Locator) -> None:
326+
nonlocal called
327+
called += 1
328+
if called == 1:
329+
button.click()
330+
else:
331+
page.locator("#interstitial").wait_for(state="hidden")
332+
333+
page.add_locator_handler(
334+
page.get_by_role("button", name="close"), _handler, no_wait_after=True
335+
)
336+
page.locator("#aside").hover()
337+
page.evaluate(
338+
"""
339+
() => {
340+
window.clicked = 0;
341+
window.setupAnnoyingInterstitial('timeout', 1);
342+
}
343+
"""
344+
)
345+
page.locator("#target").click()
346+
assert page.evaluate("window.clicked") == 1
347+
expect(page.locator("#interstitial")).not_to_be_visible()
348+
assert called == 2
349+
350+
351+
def test_should_removeLocatorHandler(page: Page, server: Server) -> None:
352+
page.goto(server.PREFIX + "/input/handle-locator.html")
353+
called = 0
354+
355+
def _handler(locator: Locator) -> None:
356+
nonlocal called
357+
called += 1
358+
locator.click()
359+
360+
page.add_locator_handler(page.get_by_role("button", name="close"), _handler)
361+
page.evaluate(
362+
"""
363+
() => {
364+
window.clicked = 0;
365+
window.setupAnnoyingInterstitial('hide', 1);
366+
}
367+
"""
368+
)
369+
page.locator("#target").click()
370+
assert called == 1
371+
assert page.evaluate("window.clicked") == 1
372+
expect(page.locator("#interstitial")).not_to_be_visible()
373+
page.evaluate(
374+
"""
375+
() => {
376+
window.clicked = 0;
377+
window.setupAnnoyingInterstitial('hide', 1);
378+
}
379+
"""
380+
)
381+
page.remove_locator_handler(page.get_by_role("button", name="close"))
382+
with pytest.raises(Error) as error:
383+
page.locator("#target").click(timeout=3000)
384+
assert called == 1
385+
assert page.evaluate("window.clicked") == 0
386+
expect(page.locator("#interstitial")).to_be_visible()
387+
assert "Timeout 3000ms exceeded" in error.value.message

0 commit comments

Comments
 (0)