Skip to content

Commit be9b5f7

Browse files
authored
fix: request interception (#1335)
1 parent f25b954 commit be9b5f7

File tree

5 files changed

+344
-1
lines changed

5 files changed

+344
-1
lines changed

playwright/_impl/_fetch.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
is_safe_close_error,
4040
locals_to_params,
4141
object_to_array,
42+
to_impl,
4243
)
4344
from playwright._impl._network import serialize_headers
4445
from playwright._impl._tracing import Tracing
@@ -242,7 +243,11 @@ async def fetch(
242243
failOnStatusCode: bool = None,
243244
ignoreHTTPSErrors: bool = None,
244245
) -> "APIResponse":
245-
request = urlOrRequest if isinstance(urlOrRequest, network.Request) else None
246+
request = (
247+
cast(network.Request, to_impl(urlOrRequest))
248+
if isinstance(to_impl(urlOrRequest), network.Request)
249+
else None
250+
)
246251
assert request or isinstance(
247252
urlOrRequest, str
248253
), "First argument must be either URL string or Request"

tests/async/test_request_fulfill.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from playwright.async_api import Page, Route
16+
from tests.server import Server
17+
18+
19+
async def test_should_fetch_original_request_and_fulfill(page: Page, server: Server):
20+
async def handle(route: Route):
21+
response = await page.request.fetch(route.request)
22+
await route.fulfill(response=response)
23+
24+
await page.route("**/*", handle)
25+
response = await page.goto(server.PREFIX + "/title.html")
26+
assert response.status == 200
27+
assert await page.title() == "Woof-Woof"

tests/async/test_request_intercept.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
from pathlib import Path
17+
18+
from twisted.web import http
19+
20+
from playwright.async_api import Page, Route
21+
from tests.server import Server
22+
23+
24+
async def test_should_fulfill_intercepted_response(page: Page, server: Server):
25+
async def handle(route: Route):
26+
response = await page.request.fetch(route.request)
27+
await route.fulfill(
28+
response=response,
29+
status=201,
30+
headers={"foo": "bar"},
31+
content_type="text/plain",
32+
body="Yo, page!",
33+
)
34+
35+
await page.route("**/*", handle)
36+
response = await page.goto(server.PREFIX + "/empty.html")
37+
assert response.status == 201
38+
assert response.headers["foo"] == "bar"
39+
assert response.headers["content-type"] == "text/plain"
40+
assert await page.evaluate("() => document.body.textContent") == "Yo, page!"
41+
42+
43+
async def test_should_fulfill_response_with_empty_body(page: Page, server: Server):
44+
async def handle(route: Route):
45+
response = await page.request.fetch(route.request)
46+
await route.fulfill(response=response, status=201, body="")
47+
48+
await page.route("**/*", handle)
49+
response = await page.goto(server.PREFIX + "/title.html")
50+
assert response.status == 201
51+
assert await response.text() == ""
52+
53+
54+
async def test_should_override_with_defaults_when_intercepted_response_not_provided(
55+
page: Page, server: Server, browser_name: str
56+
):
57+
def server_handler(request: http.Request):
58+
request.setHeader("foo", "bar")
59+
request.write("my content".encode())
60+
request.finish()
61+
62+
server.set_route("/empty.html", server_handler)
63+
64+
async def handle(route: Route):
65+
await page.request.fetch(route.request)
66+
await route.fulfill(status=201)
67+
68+
await page.route("**/*", handle)
69+
response = await page.goto(server.EMPTY_PAGE)
70+
assert response.status == 201
71+
assert await response.text() == ""
72+
if browser_name == "webkit":
73+
assert response.headers == {"content-type": "text/plain"}
74+
else:
75+
assert response.headers == {}
76+
77+
78+
async def test_should_fulfill_with_any_response(page: Page, server: Server):
79+
def server_handler(request: http.Request):
80+
request.setHeader("foo", "bar")
81+
request.write("Woo-hoo".encode())
82+
request.finish()
83+
84+
server.set_route("/sample", server_handler)
85+
sample_response = await page.request.get(server.PREFIX + "/sample")
86+
await page.route(
87+
"**/*",
88+
lambda route: route.fulfill(
89+
response=sample_response, status=201, content_type="text/plain"
90+
),
91+
)
92+
response = await page.goto(server.EMPTY_PAGE)
93+
assert response.status == 201
94+
assert await response.text() == "Woo-hoo"
95+
assert response.headers["foo"] == "bar"
96+
97+
98+
async def test_should_support_fulfill_after_intercept(
99+
page: Page, server: Server, assetdir: Path
100+
):
101+
request_future = asyncio.create_task(server.wait_for_request("/title.html"))
102+
103+
async def handle_route(route: Route):
104+
response = await page.request.fetch(route.request)
105+
await route.fulfill(response=response)
106+
107+
await page.route("**", handle_route)
108+
response = await page.goto(server.PREFIX + "/title.html")
109+
request = await request_future
110+
assert request.uri.decode() == "/title.html"
111+
original = (assetdir / "title.html").read_text()
112+
assert await response.text() == original
113+
114+
115+
async def test_should_give_access_to_the_intercepted_response(
116+
page: Page, server: Server
117+
):
118+
await page.goto(server.EMPTY_PAGE)
119+
120+
route_task = asyncio.Future()
121+
await page.route("**/title.html", lambda route: route_task.set_result(route))
122+
123+
eval_task = asyncio.create_task(
124+
page.evaluate("url => fetch(url)", server.PREFIX + "/title.html")
125+
)
126+
127+
route = await route_task
128+
response = await page.request.fetch(route.request)
129+
130+
assert response.status == 200
131+
assert response.status_text == "OK"
132+
assert response.ok is True
133+
assert response.url.endswith("/title.html") is True
134+
assert response.headers["content-type"] == "text/html"
135+
assert list(
136+
filter(
137+
lambda header: header["name"].lower() == "content-type",
138+
response.headers_array,
139+
)
140+
) == [{"name": "Content-Type", "value": "text/html"}]
141+
142+
await asyncio.gather(
143+
route.fulfill(response=response),
144+
eval_task,
145+
)
146+
147+
148+
async def test_should_give_access_to_the_intercepted_response_body(
149+
page: Page, server: Server
150+
):
151+
await page.goto(server.EMPTY_PAGE)
152+
153+
route_task = asyncio.Future()
154+
await page.route("**/simple.json", lambda route: route_task.set_result(route))
155+
156+
eval_task = asyncio.create_task(
157+
page.evaluate("url => fetch(url)", server.PREFIX + "/simple.json")
158+
)
159+
160+
route = await route_task
161+
response = await page.request.fetch(route.request)
162+
163+
assert await response.text() == '{"foo": "bar"}\n'
164+
165+
await asyncio.gather(
166+
route.fulfill(response=response),
167+
eval_task,
168+
)

tests/sync/test_request_fulfill.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from playwright.sync_api import Page, Route
16+
from tests.server import Server
17+
18+
19+
def test_should_fetch_original_request_and_fulfill(page: Page, server: Server) -> None:
20+
def handle(route: Route) -> None:
21+
response = page.request.fetch(route.request)
22+
route.fulfill(response=response)
23+
24+
page.route("**/*", handle)
25+
response = page.goto(server.PREFIX + "/title.html")
26+
assert response
27+
assert response.status == 200
28+
assert page.title() == "Woof-Woof"

tests/sync/test_request_intercept.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from pathlib import Path
16+
17+
from twisted.web import http
18+
19+
from playwright.sync_api import Page, Route
20+
from tests.server import Server
21+
22+
23+
def test_should_fulfill_intercepted_response(page: Page, server: Server) -> None:
24+
def handle(route: Route) -> None:
25+
response = page.request.fetch(route.request)
26+
route.fulfill(
27+
response=response,
28+
status=201,
29+
headers={"foo": "bar"},
30+
content_type="text/plain",
31+
body="Yo, page!",
32+
)
33+
34+
page.route("**/*", handle)
35+
response = page.goto(server.PREFIX + "/empty.html")
36+
assert response
37+
assert response.status == 201
38+
assert response.headers["foo"] == "bar"
39+
assert response.headers["content-type"] == "text/plain"
40+
assert page.evaluate("() => document.body.textContent") == "Yo, page!"
41+
42+
43+
def test_should_fulfill_response_with_empty_body(page: Page, server: Server) -> None:
44+
def handle(route: Route) -> None:
45+
response = page.request.fetch(route.request)
46+
route.fulfill(response=response, status=201, body="")
47+
48+
page.route("**/*", handle)
49+
response = page.goto(server.PREFIX + "/title.html")
50+
assert response
51+
assert response.status == 201
52+
assert response.text() == ""
53+
54+
55+
def test_should_override_with_defaults_when_intercepted_response_not_provided(
56+
page: Page, server: Server, browser_name: str
57+
) -> None:
58+
def server_handler(request: http.Request) -> None:
59+
request.setHeader("foo", "bar")
60+
request.write("my content".encode())
61+
request.finish()
62+
63+
server.set_route("/empty.html", server_handler)
64+
65+
def handle(route: Route) -> None:
66+
page.request.fetch(route.request)
67+
route.fulfill(status=201)
68+
69+
page.route("**/*", handle)
70+
response = page.goto(server.EMPTY_PAGE)
71+
assert response
72+
assert response.status == 201
73+
assert response.text() == ""
74+
if browser_name == "webkit":
75+
assert response.headers == {"content-type": "text/plain"}
76+
else:
77+
assert response.headers == {}
78+
79+
80+
def test_should_fulfill_with_any_response(page: Page, server: Server) -> None:
81+
def server_handler(request: http.Request) -> None:
82+
request.setHeader("foo", "bar")
83+
request.write("Woo-hoo".encode())
84+
request.finish()
85+
86+
server.set_route("/sample", server_handler)
87+
sample_response = page.request.get(server.PREFIX + "/sample")
88+
page.route(
89+
"**/*",
90+
lambda route: route.fulfill(
91+
response=sample_response, status=201, content_type="text/plain"
92+
),
93+
)
94+
response = page.goto(server.EMPTY_PAGE)
95+
assert response
96+
assert response.status == 201
97+
assert response.text() == "Woo-hoo"
98+
assert response.headers["foo"] == "bar"
99+
100+
101+
def test_should_support_fulfill_after_intercept(
102+
page: Page, server: Server, assetdir: Path
103+
) -> None:
104+
def handle_route(route: Route) -> None:
105+
response = page.request.fetch(route.request)
106+
route.fulfill(response=response)
107+
108+
page.route("**", handle_route)
109+
with server.expect_request("/title.html") as request_info:
110+
response = page.goto(server.PREFIX + "/title.html")
111+
assert response
112+
request = request_info.value
113+
assert request.uri.decode() == "/title.html"
114+
original = (assetdir / "title.html").read_text()
115+
assert response.text() == original

0 commit comments

Comments
 (0)