Skip to content

Commit bb9b09f

Browse files
authored
chore: support client side event logging (#827)
1 parent 1882195 commit bb9b09f

File tree

3 files changed

+86
-5
lines changed

3 files changed

+86
-5
lines changed

playwright/_impl/_frame.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ def expect_navigation(
153153
timeout = self._page._timeout_settings.navigation_timeout()
154154
deadline = monotonic_time() + timeout
155155
wait_helper = self._setup_navigation_wait_helper("expect_navigation", timeout)
156+
157+
to_url = f' to "{url}"' if url else ""
158+
wait_helper.log(f"waiting for navigation{to_url} until '{wait_until}'")
156159
matcher = (
157160
URLMatcher(self._page._browser_context._options.get("baseURL"), url)
158161
if url
@@ -163,6 +166,7 @@ def predicate(event: Any) -> bool:
163166
# Any failed navigation results in a rejection.
164167
if event.get("error"):
165168
return True
169+
wait_helper.log(f' navigated to "{event["url"]}"')
166170
return not matcher or matcher.matches(event["url"])
167171

168172
wait_helper.wait_for_event(
@@ -211,8 +215,15 @@ async def wait_for_load_state(
211215
if state in self._load_states:
212216
return
213217
wait_helper = self._setup_navigation_wait_helper("wait_for_load_state", timeout)
218+
219+
def handle_load_state_event(actual_state: str) -> bool:
220+
wait_helper.log(f'"{actual_state}" event fired')
221+
return actual_state == state
222+
214223
wait_helper.wait_for_event(
215-
self._event_emitter, "loadstate", lambda s: s == state
224+
self._event_emitter,
225+
"loadstate",
226+
handle_load_state_event,
216227
)
217228
await wait_helper.result()
218229

playwright/_impl/_page.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import asyncio
1616
import base64
1717
import inspect
18+
import re
1819
import sys
1920
from pathlib import Path
2021
from types import SimpleNamespace
@@ -787,13 +788,26 @@ def expect_event(
787788
event: str,
788789
predicate: Callable = None,
789790
timeout: float = None,
791+
) -> EventContextManagerImpl:
792+
return self._expect_event(
793+
event, predicate, timeout, f'waiting for event "{event}"'
794+
)
795+
796+
def _expect_event(
797+
self,
798+
event: str,
799+
predicate: Callable = None,
800+
timeout: float = None,
801+
log_line: str = None,
790802
) -> EventContextManagerImpl:
791803
if timeout is None:
792804
timeout = self._timeout_settings.timeout()
793805
wait_helper = WaitHelper(self, f"page.expect_event({event})")
794806
wait_helper.reject_on_timeout(
795807
timeout, f'Timeout while waiting for event "{event}"'
796808
)
809+
if log_line:
810+
wait_helper.log(log_line)
797811
if event != Page.Events.Crash:
798812
wait_helper.reject_on_event(self, Page.Events.Crash, Error("Page crashed"))
799813
if event != Page.Events.Close:
@@ -858,8 +872,13 @@ def my_predicate(request: Request) -> bool:
858872
return predicate(request)
859873
return True
860874

861-
return self.expect_event(
862-
Page.Events.Request, predicate=my_predicate, timeout=timeout
875+
trimmed_url = trim_url(url_or_predicate)
876+
log_line = f"waiting for request {trimmed_url}" if trimmed_url else None
877+
return self._expect_event(
878+
Page.Events.Request,
879+
predicate=my_predicate,
880+
timeout=timeout,
881+
log_line=log_line,
863882
)
864883

865884
def expect_request_finished(
@@ -892,8 +911,13 @@ def my_predicate(response: Response) -> bool:
892911
return predicate(response)
893912
return True
894913

895-
return self.expect_event(
896-
Page.Events.Response, predicate=my_predicate, timeout=timeout
914+
trimmed_url = trim_url(url_or_predicate)
915+
log_line = f"waiting for response {trimmed_url}" if trimmed_url else None
916+
return self._expect_event(
917+
Page.Events.Response,
918+
predicate=my_predicate,
919+
timeout=timeout,
920+
log_line=log_line,
897921
)
898922

899923
def expect_websocket(
@@ -986,3 +1010,17 @@ async def call(self, func: Callable) -> None:
9861010
"reject", dict(error=dict(error=serialize_error(e, tb)))
9871011
)
9881012
)
1013+
1014+
1015+
def trim_url(param: URLMatchRequest) -> Optional[str]:
1016+
if isinstance(param, re.Pattern):
1017+
return trim_end(param.pattern)
1018+
if isinstance(param, str):
1019+
return trim_end(param)
1020+
return None
1021+
1022+
1023+
def trim_end(s: str) -> str:
1024+
if len(s) > 50:
1025+
return s[:50] + "\u2026"
1026+
return s

playwright/_impl/_wait_helper.py

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

1515
import asyncio
16+
import math
1617
import uuid
1718
from asyncio.tasks import Task
1819
from typing import Any, Callable, List, Tuple
@@ -31,6 +32,7 @@ def __init__(self, channel_owner: ChannelOwner, event: str) -> None:
3132
self._pending_tasks: List[Task] = []
3233
self._channel = channel_owner._channel
3334
self._registered_listeners: List[Tuple[EventEmitter, str, Callable]] = []
35+
self._logs: List[str] = []
3436
self._wait_for_event_info_before(self._wait_id, event)
3537

3638
def _wait_for_event_info_before(self, wait_id: str, event: str) -> None:
@@ -101,6 +103,9 @@ def _fulfill(self, result: Any) -> None:
101103

102104
def _reject(self, exception: Exception) -> None:
103105
self._cleanup()
106+
if exception:
107+
base_class = TimeoutError if isinstance(exception, TimeoutError) else Error
108+
exception = base_class(str(exception) + format_log_recording(self._logs))
104109
if not self._result.done():
105110
self._result.set_exception(exception)
106111
self._wait_for_event_info_after(self._wait_id, exception)
@@ -121,10 +126,37 @@ def listener(event_data: Any = None) -> None:
121126
def result(self) -> asyncio.Future:
122127
return self._result
123128

129+
def log(self, message: str) -> None:
130+
self._logs.append(message)
131+
try:
132+
self._channel.send_no_reply(
133+
"waitForEventInfo",
134+
{
135+
"info": {
136+
"waitId": self._wait_id,
137+
"phase": "log",
138+
"message": message,
139+
},
140+
},
141+
)
142+
except Exception:
143+
pass
144+
124145

125146
def throw_on_timeout(timeout: float, exception: Exception) -> asyncio.Task:
126147
async def throw() -> None:
127148
await asyncio.sleep(timeout / 1000)
128149
raise exception
129150

130151
return asyncio.create_task(throw())
152+
153+
154+
def format_log_recording(log: List[str]) -> str:
155+
if not log:
156+
return ""
157+
header = " logs "
158+
header_length = 60
159+
left_length = math.floor((header_length - len(header)) / 2)
160+
right_length = header_length - len(header) - left_length
161+
new_line = "\n"
162+
return f"{new_line}{'=' * left_length}{header}{'=' * right_length}{new_line}{new_line.join(log)}{new_line}{'=' * header_length}"

0 commit comments

Comments
 (0)