Skip to content

Commit f672611

Browse files
authored
feat(timing): introduce resource timing (microsoft#262)
1 parent de5f967 commit f672611

File tree

11 files changed

+477
-23
lines changed

11 files changed

+477
-23
lines changed

playwright/async_api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
PdfMargins,
5151
ProxyServer,
5252
RequestFailure,
53+
ResourceTiming,
5354
SelectOption,
5455
Viewport,
5556
)
@@ -209,6 +210,18 @@ def failure(self) -> typing.Union[RequestFailure, NoneType]:
209210
"""
210211
return mapping.from_maybe_impl(self._impl_obj.failure)
211212

213+
@property
214+
def timing(self) -> ResourceTiming:
215+
"""Request.timing
216+
217+
Returns resource timing information for given request. Most of the timing values become available upon the response, `responseEnd` becomes available when request finishes. Find more information at Resource Timing API.
218+
219+
Returns
220+
-------
221+
{"startTime": float, "domainLookupStart": float, "domainLookupEnd": float, "connectStart": float, "secureConnectionStart": float, "connectEnd": float, "requestStart": float, "responseStart": float, "responseEnd": float}
222+
"""
223+
return mapping.from_maybe_impl(self._impl_obj.timing)
224+
212225
async def response(self) -> typing.Union["Response", NoneType]:
213226
"""Request.response
214227

playwright/helper.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,16 @@ class MousePosition(TypedDict):
5656
y: float
5757

5858

59-
class FrameTapParams(TypedDict):
60-
selector: str
61-
force: Optional[bool]
62-
noWaitAfter: bool
63-
modifiers: Optional[List[KeyboardModifier]]
64-
position: Optional[MousePosition]
65-
timeout: int
66-
67-
68-
class FrameTapOptions(TypedDict):
69-
force: Optional[bool]
70-
noWaitAfter: bool
71-
modifiers: Optional[List[KeyboardModifier]]
72-
position: Optional[MousePosition]
73-
timeout: int
59+
class ResourceTiming(TypedDict):
60+
startTime: float
61+
domainLookupStart: float
62+
domainLookupEnd: float
63+
connectStart: float
64+
secureConnectionStart: float
65+
connectEnd: float
66+
requestStart: float
67+
responseStart: float
68+
responseEnd: float
7469

7570

7671
class FilePayload(TypedDict):

playwright/network.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
Error,
2626
Header,
2727
RequestFailure,
28+
ResourceTiming,
2829
locals_to_params,
2930
)
3031

@@ -44,6 +45,17 @@ def __init__(
4445
if self._redirected_from:
4546
self._redirected_from._redirected_to = self
4647
self._failure_text: Optional[str] = None
48+
self._timing: ResourceTiming = {
49+
"startTime": 0,
50+
"domainLookupStart": -1,
51+
"domainLookupEnd": -1,
52+
"connectStart": -1,
53+
"secureConnectionStart": -1,
54+
"connectEnd": -1,
55+
"requestStart": -1,
56+
"responseStart": -1,
57+
"responseEnd": -1,
58+
}
4759

4860
@property
4961
def url(self) -> str:
@@ -109,6 +121,10 @@ def redirectedTo(self) -> Optional["Request"]:
109121
def failure(self) -> Optional[RequestFailure]:
110122
return {"errorText": self._failure_text} if self._failure_text else None
111123

124+
@property
125+
def timing(self) -> ResourceTiming:
126+
return self._timing
127+
112128

113129
class Route(ChannelOwner):
114130
def __init__(
@@ -183,6 +199,16 @@ def __init__(
183199
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
184200
) -> None:
185201
super().__init__(parent, type, guid, initializer)
202+
self._request: Request = from_channel(self._initializer["request"])
203+
timing = self._initializer["timing"]
204+
self._request._timing["startTime"] = timing["startTime"]
205+
self._request._timing["domainLookupStart"] = timing["domainLookupStart"]
206+
self._request._timing["domainLookupEnd"] = timing["domainLookupEnd"]
207+
self._request._timing["connectStart"] = timing["connectStart"]
208+
self._request._timing["secureConnectionStart"] = timing["secureConnectionStart"]
209+
self._request._timing["connectEnd"] = timing["connectEnd"]
210+
self._request._timing["requestStart"] = timing["requestStart"]
211+
self._request._timing["responseStart"] = timing["responseStart"]
186212

187213
@property
188214
def url(self) -> str:
@@ -222,11 +248,11 @@ async def json(self) -> Union[Dict, List]:
222248

223249
@property
224250
def request(self) -> Request:
225-
return from_channel(self._initializer["request"])
251+
return self._request
226252

227253
@property
228254
def frame(self) -> "Frame":
229-
return self.request.frame
255+
return self._request.frame
230256

231257

232258
def serialize_headers(headers: Dict[str, str]) -> List[Header]:

playwright/page.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,15 @@ def __init__(
184184
self._channel.on(
185185
"requestFailed",
186186
lambda params: self._on_request_failed(
187-
from_channel(params["request"]), params["failureText"]
187+
from_channel(params["request"]),
188+
params["responseEndTiming"],
189+
params["failureText"],
188190
),
189191
)
190192
self._channel.on(
191193
"requestFinished",
192-
lambda params: self.emit(
193-
Page.Events.RequestFinished, from_channel(params["request"])
194+
lambda params: self._on_request_finished(
195+
from_channel(params["request"]), params["responseEndTiming"]
194196
),
195197
)
196198
self._channel.on(
@@ -219,10 +221,24 @@ def _set_browser_context(self, context: "BrowserContext") -> None:
219221
self._browser_context = context
220222
self._timeout_settings = TimeoutSettings(context._timeout_settings)
221223

222-
def _on_request_failed(self, request: Request, failure_text: str = None) -> None:
224+
def _on_request_failed(
225+
self,
226+
request: Request,
227+
response_end_timing: float,
228+
failure_text: str = None,
229+
) -> None:
223230
request._failure_text = failure_text
231+
if request._timing:
232+
request._timing["responseEnd"] = response_end_timing
224233
self.emit(Page.Events.RequestFailed, request)
225234

235+
def _on_request_finished(
236+
self, request: Request, response_end_timing: float
237+
) -> None:
238+
if request._timing:
239+
request._timing["responseEnd"] = response_end_timing
240+
self.emit(Page.Events.RequestFinished, request)
241+
226242
def _on_frame_attached(self, frame: Frame) -> None:
227243
frame._page = self
228244
self._frames.append(frame)

playwright/sync_api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
PdfMargins,
5050
ProxyServer,
5151
RequestFailure,
52+
ResourceTiming,
5253
SelectOption,
5354
Viewport,
5455
)
@@ -209,6 +210,18 @@ def failure(self) -> typing.Union[RequestFailure, NoneType]:
209210
"""
210211
return mapping.from_maybe_impl(self._impl_obj.failure)
211212

213+
@property
214+
def timing(self) -> ResourceTiming:
215+
"""Request.timing
216+
217+
Returns resource timing information for given request. Most of the timing values become available upon the response, `responseEnd` becomes available when request finishes. Find more information at Resource Timing API.
218+
219+
Returns
220+
-------
221+
{"startTime": float, "domainLookupStart": float, "domainLookupEnd": float, "connectStart": float, "secureConnectionStart": float, "connectEnd": float, "requestStart": float, "responseStart": float, "responseEnd": float}
222+
"""
223+
return mapping.from_maybe_impl(self._impl_obj.timing)
224+
212225
def response(self) -> typing.Union["Response", NoneType]:
213226
"""Request.response
214227

scripts/documentation_provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ def serialize_doc_type(
290290
return "bool"
291291

292292
if type_name == "number":
293+
if fqname == "Request.timing(return=)" or "ResourceTiming" in fqname:
294+
return "float"
293295
if ("Mouse" in fqname or "Touchscreen" in fqname) and (
294296
"(x=)" in fqname or "(y=)" in fqname
295297
):

scripts/expected_api_mismatch.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,3 @@ Parameter not implemented: Browser.newPage(recordHar=)
125125
Parameter not implemented: Browser.newContext(recordHar=)
126126
Method not implemented: WebSocket.url
127127
Parameter not implemented: BrowserType.launchPersistentContext(recordHar=)
128-
Method not implemented: Request.timing

scripts/generate_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def return_value(value: Any) -> List[str]:
163163
from playwright.element_handle import ElementHandle as ElementHandleImpl
164164
from playwright.file_chooser import FileChooser as FileChooserImpl
165165
from playwright.frame import Frame as FrameImpl
166-
from playwright.helper import ConsoleMessageLocation, Credentials, MousePosition, Error, FilePayload, SelectOption, RequestFailure, Viewport, DeviceDescriptor, IntSize, FloatRect, Geolocation, ProxyServer, PdfMargins
166+
from playwright.helper import ConsoleMessageLocation, Credentials, MousePosition, Error, FilePayload, SelectOption, RequestFailure, Viewport, DeviceDescriptor, IntSize, FloatRect, Geolocation, ProxyServer, PdfMargins, ResourceTiming
167167
from playwright.input import Keyboard as KeyboardImpl, Mouse as MouseImpl, Touchscreen as TouchscreenImpl
168168
from playwright.js_handle import JSHandle as JSHandleImpl
169169
from playwright.network import Request as RequestImpl, Response as ResponseImpl, Route as RouteImpl

scripts/test_to_python.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Copyright (c) Microsoft Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
const fs = require('fs');
19+
20+
(async () => {
21+
const content = fs.readFileSync(process.argv[2]).toString();
22+
const lines = content.split('\n');
23+
for (let line of lines) {
24+
if (line.trim().startsWith('describe')) {
25+
console.log('# DESCRIBE ' + line)
26+
continue;
27+
}
28+
if (line.trim() === '});') {
29+
console.log('');
30+
continue;
31+
}
32+
33+
line = line.replace(/isWindows/g, 'is_win');
34+
line = line.replace(/isLinux/g, 'is_linux');
35+
line = line.replace(/isMac/g, 'is_mac');
36+
line = line.replace(/isWebKit/g, 'is_webkit');
37+
line = line.replace(/isChromium/g, 'is_chromium');
38+
line = line.replace(/isFirefox/g, 'is_firefox');
39+
40+
let match = line.match(/it.*\(\'([^']+)\'.*async(?: function)?\s*\(\s*{(.*)}\s*\).*/);
41+
if (match) {
42+
console.log(`async def test_${match[1].replace(/[- =]|\[|\]|\>|\</g, '_')}(${match[2].trim()}):`);
43+
continue;
44+
}
45+
46+
line = line.replace(/;$/g, '');
47+
line = line.replace(/ const /g, ' ');
48+
line = line.replace(/ let /g, ' ');
49+
line = line.replace(/\&\&/g, 'and');
50+
line = line.replace(/\|\|/g, 'or');
51+
line = line.replace(/'/g, '"');
52+
line = line.replace(/ = null/g, ' = None');
53+
line = line.replace(/===/g, '==');
54+
line = line.replace('await Promise.all([', 'await asyncio.gather(');
55+
line = line.replace(/\.\$\(/, '.querySelector(');
56+
line = line.replace(/\.\$$\(/, '.querySelectorAll(');
57+
line = line.replace(/\.\$eval\(/, '.evalOnSelector(');
58+
line = line.replace(/\.\$$eval\(/, '.evalOnSelectorAll(');
59+
60+
match = line.match(/(\s+)expect\((.*)\).toEqual\((.*)\)/)
61+
if (match)
62+
line = `${match[1]}assert ${match[2]} == ${match[3]}`;
63+
match = line.match(/(\s+)expect\((.*)\).toBe\((.*)\)/)
64+
if (match)
65+
line = `${match[1]}assert ${match[2]} == ${match[3]}`;
66+
match = line.match(/(\s+)expect\((.*)\).toBeTruthy\((.*)\)/)
67+
if (match)
68+
line = `${match[1]}assert ${match[2]}`;
69+
match = line.match(/(\s+)expect\((.*)\).toBeGreaterThan\((.*)\)/)
70+
if (match)
71+
line = `${match[1]}assert ${match[2]} > ${match[3]}`;
72+
73+
match = line.match(/(\s+)expect\((.*)\).toBeLessThan\((.*)\)/)
74+
if (match)
75+
line = `${match[1]}assert ${match[2]} < ${match[3]}`;
76+
77+
match = line.match(/(\s+)expect\((.*)\).toBeGreaterThanOrEqual\((.*)\)/)
78+
if (match)
79+
line = `${match[1]}assert ${match[2]} >= ${match[3]}`;
80+
81+
match = line.match(/(\s+)expect\((.*)\).toBeLessThanOrEqual\((.*)\)/)
82+
if (match)
83+
line = `${match[1]}assert ${match[2]} <= ${match[3]}`;
84+
85+
line = line.replace(/ false/g, ' False');
86+
line = line.replace(/ true/g, ' True');
87+
88+
line = line.replace(/ == null/g, ' == None');
89+
if (line.trim().startsWith('assert') && line.endsWith(' == True'))
90+
line = line.substring(0, line.length - ' == True'.length);
91+
92+
// Quote evaluate
93+
let index = line.indexOf('.evaluate(');
94+
if (index !== -1) {
95+
const tokens = [line.substring(0, index) + '.evaluate(\''];
96+
let depth = 0;
97+
for (let i = index + '.evaluate('.length; i < line.length; ++i) {
98+
if (line[i] == '(')
99+
++depth;
100+
if (line[i] == ')')
101+
--depth;
102+
if (depth < 0) {
103+
tokens.push('\'' + line.substring(i));
104+
break;
105+
}
106+
if (depth === 0 && line[i] === ',') {
107+
tokens.push('\"' + line.substring(i));
108+
break;
109+
}
110+
tokens.push(line[i]);
111+
}
112+
console.log(tokens.join(''));
113+
continue;
114+
}
115+
116+
// Name keys in the dict
117+
index = line.indexOf('{');
118+
if (index !== -1) {
119+
let ok = false;
120+
for (let i = index + 1; i < line.length; ++i) {
121+
if (line[i] === '}') {
122+
try {
123+
console.log(line.substring(0, index) + JSON.stringify(eval('(' + line.substring(index, i + 1) + ')')).replace(/\"/g, '\'') + line.substring(i + 1));
124+
ok = true;
125+
break;
126+
} catch (e) {
127+
}
128+
}
129+
}
130+
if (ok) continue;
131+
}
132+
133+
// Single line template strings
134+
index = line.indexOf('`');
135+
if (index !== -1) {
136+
const tokens = [line.substring(0, index) + '\''];
137+
let ok = false;
138+
for (let i = index + 1; i < line.length; ++i) {
139+
if (line[i] === '`') {
140+
tokens.push('\'' + line.substring(i + 1));
141+
console.log(tokens.join(''));
142+
ok = true;
143+
break;
144+
}
145+
if (line[i] === '\'')
146+
tokens.push('"');
147+
else
148+
tokens.push(line[i]);
149+
}
150+
if (ok) continue;
151+
}
152+
153+
line = line.replace(/(\s+)/, '$1$1');
154+
if (line.endsWith('{'))
155+
line = line.substring(0, line.length - 1).trimEnd() + ':';
156+
if (line.trim().startsWith('}'))
157+
line = line.substring(0, line.indexOf('}')) + line.substring(line.indexOf('}') + 1).trim();
158+
if (line.trim().startsWith('//'))
159+
line = line.replace(/\/\//, '#');
160+
if (!line.trim())
161+
line = '';
162+
console.log(line);
163+
}
164+
})();

0 commit comments

Comments
 (0)