diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 774b001ec..eabece583 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,7 +22,7 @@ repos:
rev: v1.5.1
hooks:
- id: mypy
- additional_dependencies: [types-pyOpenSSL==23.2.0.2]
+ additional_dependencies: [types-pyOpenSSL==23.2.0.2, types-requests==2.31.0.10]
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
diff --git a/local-requirements.txt b/local-requirements.txt
index 68edf4cb1..4a4a27ada 100644
--- a/local-requirements.txt
+++ b/local-requirements.txt
@@ -20,4 +20,5 @@ service_identity==23.1.0
setuptools==68.2.2
twisted==23.10.0
types-pyOpenSSL==23.2.0.2
+types-requests==2.31.0.10
wheel==0.41.2
diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py
index 3f9fa5ce3..d18d0d5de 100644
--- a/playwright/_impl/_locator.py
+++ b/playwright/_impl/_locator.py
@@ -15,7 +15,6 @@
import json
import pathlib
import sys
-from collections import ChainMap
from typing import (
TYPE_CHECKING,
Any,
@@ -528,7 +527,7 @@ async def screenshot(
params = locals_to_params(locals())
return await self._with_element(
lambda h, timeout: h.screenshot(
- **ChainMap({"timeout": timeout}, params),
+ **{**params, "timeout": timeout},
),
)
@@ -561,9 +560,7 @@ async def select_option(
async def select_text(self, force: bool = None, timeout: float = None) -> None:
params = locals_to_params(locals())
return await self._with_element(
- lambda h, timeout: h.select_text(
- **ChainMap({"timeout": timeout}, params),
- ),
+ lambda h, timeout: h.select_text(**{**params, "timeout": timeout}),
timeout,
)
diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py
index 67bd9d48d..102767cf6 100644
--- a/playwright/_impl/_network.py
+++ b/playwright/_impl/_network.py
@@ -167,7 +167,7 @@ def post_data(self) -> Optional[str]:
data = self._fallback_overrides.post_data_buffer
if not data:
return None
- return data.decode() if isinstance(data, bytes) else data
+ return data.decode()
@property
def post_data_json(self) -> Optional[Any]:
diff --git a/pyproject.toml b/pyproject.toml
index da6e54e07..e87689aa0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,17 +25,16 @@ warn_unused_configs = true
check_untyped_defs = true
disallow_untyped_defs = true
no_implicit_optional = false
-
-[[tool.mypy.overrides]]
-module = "tests/async.*"
-ignore_errors = true
+exclude = [
+ "build/",
+ "env/",
+]
[tool.isort]
profile = "black"
[tool.pyright]
-include = ["playwright", "tests/sync"]
-ignore = ["tests/async/", "scripts/"]
+include = ["playwright", "tests", "scripts"]
pythonVersion = "3.8"
reportMissingImports = false
reportTypedDictNotRequiredAccess = false
diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py
index a68697be1..2d03ebc04 100644
--- a/scripts/documentation_provider.py
+++ b/scripts/documentation_provider.py
@@ -16,16 +16,7 @@
import re
import subprocess
from sys import stderr
-from typing import ( # type: ignore
- Any,
- Dict,
- List,
- Set,
- Union,
- get_args,
- get_origin,
- get_type_hints,
-)
+from typing import Any, Dict, List, Set, Union, get_args, get_origin, get_type_hints
from urllib.parse import urljoin
from playwright._impl._helper import to_snake_case
diff --git a/tests/async/test_browsercontext_add_cookies.py b/tests/async/test_browsercontext_add_cookies.py
index 744e989d1..6f457a11f 100644
--- a/tests/async/test_browsercontext_add_cookies.py
+++ b/tests/async/test_browsercontext_add_cookies.py
@@ -19,7 +19,7 @@
import pytest
from playwright.async_api import Browser, BrowserContext, Error, Page
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
from tests.utils import must
@@ -49,7 +49,7 @@ async def test_should_roundtrip_cookie(
cookies = await context.cookies()
await context.clear_cookies()
assert await context.cookies() == []
- await context.add_cookies(cookies)
+ await context.add_cookies(cookies) # type: ignore
assert await context.cookies() == cookies
@@ -58,7 +58,7 @@ async def test_should_send_cookie_header(
) -> None:
cookie: List[str] = []
- def handler(request: HttpRequestWithPostBody) -> None:
+ def handler(request: TestServerRequest) -> None:
cookie.extend(must(request.requestHeaders.getRawHeaders("cookie")))
request.finish()
@@ -154,7 +154,7 @@ async def test_should_isolate_send_cookie_header(
) -> None:
cookie: List[str] = []
- def handler(request: HttpRequestWithPostBody) -> None:
+ def handler(request: TestServerRequest) -> None:
cookie.extend(request.requestHeaders.getRawHeaders("cookie") or [])
request.finish()
diff --git a/tests/async/test_browsercontext_events.py b/tests/async/test_browsercontext_events.py
index 9cae739dc..a0a3b90eb 100644
--- a/tests/async/test_browsercontext_events.py
+++ b/tests/async/test_browsercontext_events.py
@@ -20,7 +20,7 @@
from playwright.async_api import Page
from tests.utils import must
-from ..server import HttpRequestWithPostBody, Server
+from ..server import Server, TestServerRequest
async def test_console_event_should_work(page: Page) -> None:
@@ -162,7 +162,7 @@ async def test_dialog_event_should_work_in_immdiately_closed_popup(page: Page) -
async def test_dialog_event_should_work_with_inline_script_tag(
page: Page, server: Server
) -> None:
- def handle_route(request: HttpRequestWithPostBody) -> None:
+ def handle_route(request: TestServerRequest) -> None:
request.setHeader("content-type", "text/html")
request.write(b"""""")
request.finish()
diff --git a/tests/async/test_browsercontext_proxy.py b/tests/async/test_browsercontext_proxy.py
index 07f52a562..6f2f21440 100644
--- a/tests/async/test_browsercontext_proxy.py
+++ b/tests/async/test_browsercontext_proxy.py
@@ -20,7 +20,7 @@
from flaky import flaky
from playwright.async_api import Browser, BrowserContext
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
@pytest.fixture(scope="session")
@@ -89,7 +89,7 @@ async def test_should_work_with_ip_port_notion(
async def test_should_authenticate(
context_factory: "Callable[..., Awaitable[BrowserContext]]", server: Server
) -> None:
- def handler(req: HttpRequestWithPostBody) -> None:
+ def handler(req: TestServerRequest) -> None:
auth = req.getHeader("proxy-authorization")
if not auth:
req.setHeader(
@@ -120,7 +120,7 @@ def handler(req: HttpRequestWithPostBody) -> None:
async def test_should_authenticate_with_empty_password(
context_factory: "Callable[..., Awaitable[BrowserContext]]", server: Server
) -> None:
- def handler(req: HttpRequestWithPostBody) -> None:
+ def handler(req: TestServerRequest) -> None:
auth = req.getHeader("proxy-authorization")
if not auth:
req.setHeader(
diff --git a/tests/async/test_browsertype_connect.py b/tests/async/test_browsertype_connect.py
index 556e8eefd..34bf42245 100644
--- a/tests/async/test_browsertype_connect.py
+++ b/tests/async/test_browsertype_connect.py
@@ -23,7 +23,7 @@
from playwright.async_api import BrowserType, Error, Playwright, Route
from tests.conftest import RemoteServer
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
from tests.utils import parse_trace
@@ -168,7 +168,7 @@ async def test_browser_type_connect_should_reject_navigation_when_browser_closes
async def test_should_not_allow_getting_the_path(
browser_type: BrowserType, launch_server: Callable[[], RemoteServer], server: Server
) -> None:
- def handle_download(request: HttpRequestWithPostBody) -> None:
+ def handle_download(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/octet-stream")
request.setHeader("Content-Disposition", "attachment")
request.write(b"Hello world")
diff --git a/tests/async/test_download.py b/tests/async/test_download.py
index 94a329606..96d06820e 100644
--- a/tests/async/test_download.py
+++ b/tests/async/test_download.py
@@ -20,7 +20,7 @@
import pytest
from playwright.async_api import Browser, Download, Error, Page
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE
@@ -31,13 +31,13 @@ def assert_file_content(path: Path, content: str) -> None:
@pytest.fixture(autouse=True)
def after_each_hook(server: Server) -> Generator[None, None, None]:
- def handle_download(request: HttpRequestWithPostBody) -> None:
+ def handle_download(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/octet-stream")
request.setHeader("Content-Disposition", "attachment")
request.write(b"Hello world")
request.finish()
- def handle_download_with_file_name(request: HttpRequestWithPostBody) -> None:
+ def handle_download_with_file_name(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/octet-stream")
request.setHeader("Content-Disposition", "attachment; filename=file.txt")
request.write(b"Hello world")
@@ -206,7 +206,7 @@ async def test_should_report_non_navigation_downloads(
browser: Browser, server: Server
) -> None:
# Mac WebKit embedder does not download in this case, although Safari does.
- def handle_download(request: HttpRequestWithPostBody) -> None:
+ def handle_download(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/octet-stream")
request.write(b"Hello world")
request.finish()
@@ -275,7 +275,7 @@ async def test_should_report_alt_click_downloads(
) -> None:
# Firefox does not download on alt-click by default.
# Our WebKit embedder does not download on alt-click, although Safari does.
- def handle_download(request: HttpRequestWithPostBody) -> None:
+ def handle_download(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/octet-stream")
request.write(b"Hello world")
request.finish()
@@ -365,7 +365,7 @@ async def test_should_delete_downloads_on_browser_gone(
async def test_download_cancel_should_work(browser: Browser, server: Server) -> None:
- def handle_download(request: HttpRequestWithPostBody) -> None:
+ def handle_download(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/octet-stream")
request.setHeader("Content-Disposition", "attachment")
# Chromium requires a large enough payload to trigger the download event soon enough
diff --git a/tests/async/test_fetch_browser_context.py b/tests/async/test_fetch_browser_context.py
index 999becf47..2c515697b 100644
--- a/tests/async/test_fetch_browser_context.py
+++ b/tests/async/test_fetch_browser_context.py
@@ -14,7 +14,7 @@
import asyncio
import json
-from typing import Any
+from typing import Any, cast
from urllib.parse import parse_qs
import pytest
@@ -220,7 +220,9 @@ async def test_should_support_multipart_form_data(
),
)
assert request.method == b"POST"
- assert must(request.getHeader("Content-Type")).startswith("multipart/form-data; ")
+ assert cast(str, request.getHeader("Content-Type")).startswith(
+ "multipart/form-data; "
+ )
assert must(request.getHeader("Content-Length")) == str(
len(must(request.post_body))
)
diff --git a/tests/async/test_har.py b/tests/async/test_har.py
index b0978894b..31a34f8fa 100644
--- a/tests/async/test_har.py
+++ b/tests/async/test_har.py
@@ -18,12 +18,12 @@
import re
import zipfile
from pathlib import Path
+from typing import cast
import pytest
from playwright.async_api import Browser, BrowserContext, Error, Page, Route, expect
from tests.server import Server
-from tests.utils import must
async def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None:
@@ -560,7 +560,7 @@ async def test_should_disambiguate_by_header(
) -> None:
server.set_route(
"/echo",
- lambda req: (req.write(must(req.getHeader("baz")).encode()), req.finish()),
+ lambda req: (req.write(cast(str, req.getHeader("baz")).encode()), req.finish()),
)
fetch_function = """
async (bazValue) => {
diff --git a/tests/async/test_interception.py b/tests/async/test_interception.py
index 6b0bf0a27..911d7ddd8 100644
--- a/tests/async/test_interception.py
+++ b/tests/async/test_interception.py
@@ -29,7 +29,7 @@
Request,
Route,
)
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
from tests.utils import must
@@ -412,7 +412,7 @@ async def test_page_route_should_work_with_equal_requests(
await page.goto(server.EMPTY_PAGE)
hits = [True]
- def handle_request(request: HttpRequestWithPostBody, hits: List[bool]) -> None:
+ def handle_request(request: TestServerRequest, hits: List[bool]) -> None:
request.write(str(len(hits) * 11).encode())
request.finish()
hits.append(True)
@@ -857,7 +857,7 @@ async def test_request_fulfill_should_not_modify_the_headers_sent_to_the_server(
# this is just to enable request interception, which disables caching in chromium
await page.route(server.PREFIX + "/unused", lambda route, req: None)
- def _handler1(response: HttpRequestWithPostBody) -> None:
+ def _handler1(response: TestServerRequest) -> None:
interceptedRequests.append(response)
response.setHeader("Access-Control-Allow-Origin", "*")
response.write(b"done")
diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py
index 62cc5036f..de4a2f5e9 100644
--- a/tests/async/test_navigation.py
+++ b/tests/async/test_navigation.py
@@ -29,7 +29,7 @@
Route,
TimeoutError,
)
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
async def test_goto_should_work(page: Page, server: Server) -> None:
@@ -155,7 +155,7 @@ async def test_goto_should_return_response_when_page_changes_its_url_after_load(
async def test_goto_should_work_with_subframes_return_204(
page: Page, server: Server
) -> None:
- def handle(request: HttpRequestWithPostBody) -> None:
+ def handle(request: TestServerRequest) -> None:
request.setResponseCode(204)
request.finish()
@@ -168,7 +168,7 @@ async def test_goto_should_fail_when_server_returns_204(
page: Page, server: Server, is_chromium: bool, is_webkit: bool
) -> None:
# WebKit just loads an empty page.
- def handle(request: HttpRequestWithPostBody) -> None:
+ def handle(request: TestServerRequest) -> None:
request.setResponseCode(204)
request.finish()
@@ -897,7 +897,7 @@ async def test_wait_for_load_state_in_popup(
await page.goto(server.EMPTY_PAGE)
css_requests = []
- def handle_request(request: HttpRequestWithPostBody) -> None:
+ def handle_request(request: TestServerRequest) -> None:
css_requests.append(request)
request.write(b"body {}")
request.finish()
@@ -1080,7 +1080,7 @@ async def test_reload_should_work_with_data_url(page: Page, server: Server) -> N
async def test_should_work_with__blank_target(page: Page, server: Server) -> None:
- def handler(request: HttpRequestWithPostBody) -> None:
+ def handler(request: TestServerRequest) -> None:
request.write(
f'Click me'.encode()
)
@@ -1095,7 +1095,7 @@ def handler(request: HttpRequestWithPostBody) -> None:
async def test_should_work_with_cross_process__blank_target(
page: Page, server: Server
) -> None:
- def handler(request: HttpRequestWithPostBody) -> None:
+ def handler(request: TestServerRequest) -> None:
request.write(
f'Click me'.encode()
)
diff --git a/tests/async/test_network.py b/tests/async/test_network.py
index 015372fc0..486a98914 100644
--- a/tests/async/test_network.py
+++ b/tests/async/test_network.py
@@ -23,7 +23,7 @@
from twisted.web import http
from playwright.async_api import Browser, Error, Page, Request, Response, Route
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
from .utils import Utils
@@ -631,7 +631,7 @@ async def test_network_events_request_failed(
is_mac: bool,
is_win: bool,
) -> None:
- def handle_request(request: HttpRequestWithPostBody) -> None:
+ def handle_request(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "text/css")
request.transport.loseConnection()
diff --git a/tests/async/test_page.py b/tests/async/test_page.py
index 349914b6f..376df8376 100644
--- a/tests/async/test_page.py
+++ b/tests/async/test_page.py
@@ -28,7 +28,7 @@
Route,
TimeoutError,
)
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
from tests.utils import TARGET_CLOSED_ERROR_MESSAGE, must
@@ -151,7 +151,7 @@ async def test_load_should_fire_when_expected(page: Page) -> None:
async def test_should_work_with_wait_for_loadstate(page: Page, server: Server) -> None:
messages = []
- def _handler(request: HttpRequestWithPostBody) -> None:
+ def _handler(request: TestServerRequest) -> None:
messages.append("route")
request.setHeader("Content-Type", "text/html")
request.write(b"")
diff --git a/tests/async/test_page_network_response.py b/tests/async/test_page_network_response.py
index 98f4aaa42..58988fabc 100644
--- a/tests/async/test_page_network_response.py
+++ b/tests/async/test_page_network_response.py
@@ -17,7 +17,7 @@
import pytest
from playwright.async_api import Error, Page
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
async def test_should_reject_response_finished_if_page_closes(
@@ -25,7 +25,7 @@ async def test_should_reject_response_finished_if_page_closes(
) -> None:
await page.goto(server.EMPTY_PAGE)
- def handle_get(request: HttpRequestWithPostBody) -> None:
+ def handle_get(request: TestServerRequest) -> None:
# In Firefox, |fetch| will be hanging until it receives |Content-Type| header
# from server.
request.setHeader("Content-Type", "text/plain; charset=utf-8")
@@ -51,7 +51,7 @@ async def test_should_reject_response_finished_if_context_closes(
) -> None:
await page.goto(server.EMPTY_PAGE)
- def handle_get(request: HttpRequestWithPostBody) -> None:
+ def handle_get(request: TestServerRequest) -> None:
# In Firefox, |fetch| will be hanging until it receives |Content-Type| header
# from server.
request.setHeader("Content-Type", "text/plain; charset=utf-8")
diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py
index 2206135be..934aed8a0 100644
--- a/tests/async/test_page_request_intercept.py
+++ b/tests/async/test_page_request_intercept.py
@@ -18,13 +18,13 @@
import pytest
from playwright.async_api import Error, Page, Route, expect
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
async def test_should_support_timeout_option_in_route_fetch(
server: Server, page: Page
) -> None:
- def _handler(request: HttpRequestWithPostBody) -> None:
+ def _handler(request: TestServerRequest) -> None:
request.responseHeaders.addRawHeader("Content-Length", "4096")
request.responseHeaders.addRawHeader("Content-Type", "text/html")
request.write(b"")
diff --git a/tests/async/test_proxy.py b/tests/async/test_proxy.py
index e1c072e9d..d85613964 100644
--- a/tests/async/test_proxy.py
+++ b/tests/async/test_proxy.py
@@ -19,7 +19,7 @@
import pytest
from playwright.async_api import Browser, Error
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
async def test_should_throw_for_bad_server_value(
@@ -86,7 +86,7 @@ async def test_should_work_with_ip_port_notion(
async def test_should_authenticate(
browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
) -> None:
- def handler(req: HttpRequestWithPostBody) -> None:
+ def handler(req: TestServerRequest) -> None:
auth = req.getHeader("proxy-authorization")
if not auth:
req.setHeader(
@@ -116,7 +116,7 @@ def handler(req: HttpRequestWithPostBody) -> None:
async def test_should_authenticate_with_empty_password(
browser_factory: "Callable[..., asyncio.Future[Browser]]", server: Server
) -> None:
- def handler(req: HttpRequestWithPostBody) -> None:
+ def handler(req: TestServerRequest) -> None:
auth = req.getHeader("proxy-authorization")
if not auth:
req.setHeader(
diff --git a/tests/server.py b/tests/server.py
index 37d2c2b0d..06e344653 100644
--- a/tests/server.py
+++ b/tests/server.py
@@ -31,18 +31,21 @@
Set,
Tuple,
TypeVar,
+ cast,
)
from urllib.parse import urlparse
from autobahn.twisted.websocket import WebSocketServerFactory, WebSocketServerProtocol
from OpenSSL import crypto
-from twisted.internet import reactor, ssl
-from twisted.internet.protocol import ClientFactory
+from twisted.internet import reactor as _twisted_reactor
+from twisted.internet import ssl
+from twisted.internet.selectreactor import SelectReactor
from twisted.web import http
from playwright._impl._path_utils import get_file_dirname
_dirname = get_file_dirname()
+reactor = cast(SelectReactor, _twisted_reactor)
def find_free_port() -> int:
@@ -52,10 +55,6 @@ def find_free_port() -> int:
return s.getsockname()[1]
-class HttpRequestWithPostBody(http.Request):
- post_body: Optional[bytes] = None
-
-
T = TypeVar("T")
@@ -70,6 +69,76 @@ def value(self) -> T:
return self._value
+class TestServerRequest(http.Request):
+ __test__ = False
+ channel: "TestServerHTTPChannel"
+ post_body: Optional[bytes] = None
+
+ def process(self) -> None:
+ server = self.channel.factory.server_instance
+ if self.content:
+ self.post_body = self.content.read()
+ self.content.seek(0, 0)
+ else:
+ self.post_body = None
+ uri = urlparse(self.uri.decode())
+ path = uri.path
+
+ request_subscriber = server.request_subscribers.get(path)
+ if request_subscriber:
+ request_subscriber._loop.call_soon_threadsafe(
+ request_subscriber.set_result, self
+ )
+ server.request_subscribers.pop(path)
+
+ if server.auth.get(path):
+ authorization_header = self.requestHeaders.getRawHeaders("authorization")
+ creds_correct = False
+ if authorization_header:
+ creds_correct = server.auth.get(path) == (
+ self.getUser().decode(),
+ self.getPassword().decode(),
+ )
+ if not creds_correct:
+ self.setHeader(b"www-authenticate", 'Basic realm="Secure Area"')
+ self.setResponseCode(HTTPStatus.UNAUTHORIZED)
+ self.finish()
+ return
+ if server.csp.get(path):
+ self.setHeader(b"Content-Security-Policy", server.csp[path])
+ if server.routes.get(path):
+ server.routes[path](self)
+ return
+ file_content = None
+ try:
+ file_content = (server.static_path / path[1:]).read_bytes()
+ content_type = mimetypes.guess_type(path)[0]
+ if content_type and content_type.startswith("text/"):
+ content_type += "; charset=utf-8"
+ self.setHeader(b"Content-Type", content_type)
+ self.setHeader(b"Cache-Control", "no-cache, no-store")
+ if path in server.gzip_routes:
+ self.setHeader("Content-Encoding", "gzip")
+ self.write(gzip.compress(file_content))
+ else:
+ self.setHeader(b"Content-Length", str(len(file_content)))
+ self.write(file_content)
+ self.setResponseCode(HTTPStatus.OK)
+ except (FileNotFoundError, IsADirectoryError, PermissionError):
+ self.setResponseCode(HTTPStatus.NOT_FOUND)
+ self.finish()
+
+
+class TestServerHTTPChannel(http.HTTPChannel):
+ factory: "TestServerFactory"
+ requestFactory = TestServerRequest
+
+
+class TestServerFactory(http.HTTPFactory):
+ server_instance: "Server"
+ protocol = TestServerHTTPChannel
+
+
class Server:
protocol = "http"
@@ -89,103 +158,39 @@ def __repr__(self) -> str:
return self.PREFIX
@abc.abstractmethod
- def listen(self, factory: ClientFactory) -> None:
+ def listen(self, factory: TestServerFactory) -> None:
pass
def start(self) -> None:
request_subscribers: Dict[str, asyncio.Future] = {}
auth: Dict[str, Tuple[str, str]] = {}
csp: Dict[str, str] = {}
- routes: Dict[str, Callable[[HttpRequestWithPostBody], Any]] = {}
+ routes: Dict[str, Callable[[TestServerRequest], Any]] = {}
gzip_routes: Set[str] = set()
self.request_subscribers = request_subscribers
self.auth = auth
self.csp = csp
self.routes = routes
self.gzip_routes = gzip_routes
- static_path = _dirname / "assets"
-
- class TestServerHTTPHandler(http.Request):
- def process(self) -> None:
- request = self
- if request.content:
- self.post_body = request.content.read()
- request.content.seek(0, 0)
- else:
- self.post_body = None
- uri = urlparse(request.uri.decode())
- path = uri.path
-
- request_subscriber = request_subscribers.get(path)
- if request_subscriber:
- request_subscriber._loop.call_soon_threadsafe(
- request_subscriber.set_result, request
- )
- request_subscribers.pop(path)
-
- if auth.get(path):
- authorization_header = request.requestHeaders.getRawHeaders(
- "authorization"
- )
- creds_correct = False
- if authorization_header:
- creds_correct = auth.get(path) == (
- request.getUser().decode(),
- request.getPassword().decode(),
- )
- if not creds_correct:
- request.setHeader(
- b"www-authenticate", 'Basic realm="Secure Area"'
- )
- request.setResponseCode(HTTPStatus.UNAUTHORIZED)
- request.finish()
- return
- if csp.get(path):
- request.setHeader(b"Content-Security-Policy", csp[path])
- if routes.get(path):
- routes[path](request)
- return
- file_content = None
- try:
- file_content = (static_path / path[1:]).read_bytes()
- content_type = mimetypes.guess_type(path)[0]
- if content_type and content_type.startswith("text/"):
- content_type += "; charset=utf-8"
- request.setHeader(b"Content-Type", content_type)
- request.setHeader(b"Cache-Control", "no-cache, no-store")
- if path in gzip_routes:
- request.setHeader("Content-Encoding", "gzip")
- request.write(gzip.compress(file_content))
- else:
- request.setHeader(b"Content-Length", str(len(file_content)))
- request.write(file_content)
- self.setResponseCode(HTTPStatus.OK)
- except (FileNotFoundError, IsADirectoryError, PermissionError):
- request.setResponseCode(HTTPStatus.NOT_FOUND)
- self.finish()
-
- class MyHttp(http.HTTPChannel):
- requestFactory = TestServerHTTPHandler
-
- class MyHttpFactory(http.HTTPFactory):
- protocol = MyHttp
-
- self.listen(MyHttpFactory())
+ self.static_path = _dirname / "assets"
+ factory = TestServerFactory()
+ factory.server_instance = self
+ self.listen(factory)
- async def wait_for_request(self, path: str) -> HttpRequestWithPostBody:
+ async def wait_for_request(self, path: str) -> TestServerRequest:
if path in self.request_subscribers:
return await self.request_subscribers[path]
- future: asyncio.Future["HttpRequestWithPostBody"] = asyncio.Future()
+ future: asyncio.Future["TestServerRequest"] = asyncio.Future()
self.request_subscribers[path] = future
return await future
@contextlib.contextmanager
def expect_request(
self, path: str
- ) -> Generator[ExpectResponse[HttpRequestWithPostBody], None, None]:
+ ) -> Generator[ExpectResponse[TestServerRequest], None, None]:
future = asyncio.create_task(self.wait_for_request(path))
- cb_wrapper: ExpectResponse[HttpRequestWithPostBody] = ExpectResponse()
+ cb_wrapper: ExpectResponse[TestServerRequest] = ExpectResponse()
def done_cb(task: asyncio.Task) -> None:
cb_wrapper._value = future.result()
@@ -207,7 +212,7 @@ def reset(self) -> None:
self.routes.clear()
def set_route(
- self, path: str, callback: Callable[[HttpRequestWithPostBody], Any]
+ self, path: str, callback: Callable[[TestServerRequest], Any]
) -> None:
self.routes[path] = callback
@@ -224,7 +229,7 @@ def handle_redirect(request: http.Request) -> None:
class HTTPServer(Server):
- def listen(self, factory: ClientFactory) -> None:
+ def listen(self, factory: http.HTTPFactory) -> None:
reactor.listenTCP(self.PORT, factory, interface="127.0.0.1")
try:
reactor.listenTCP(self.PORT, factory, interface="::1")
@@ -235,7 +240,7 @@ def listen(self, factory: ClientFactory) -> None:
class HTTPSServer(Server):
protocol = "https"
- def listen(self, factory: ClientFactory) -> None:
+ def listen(self, factory: http.HTTPFactory) -> None:
cert = ssl.PrivateCertificate.fromCertificateAndKeyPair(
ssl.Certificate.loadPEM(
(_dirname / "testserver" / "cert.pem").read_bytes()
@@ -295,7 +300,7 @@ def start(self) -> None:
self.https_server.start()
self.ws_server.start()
self.thread = threading.Thread(
- target=lambda: reactor.run(installSignalHandlers=0)
+ target=lambda: reactor.run(installSignalHandlers=False)
)
self.thread.start()
diff --git a/tests/sync/test_browsercontext_events.py b/tests/sync/test_browsercontext_events.py
index 6d0840e6a..315fff0dc 100644
--- a/tests/sync/test_browsercontext_events.py
+++ b/tests/sync/test_browsercontext_events.py
@@ -18,7 +18,7 @@
from playwright.sync_api import Dialog, Page
-from ..server import HttpRequestWithPostBody, Server
+from ..server import Server, TestServerRequest
def test_console_event_should_work(page: Page) -> None:
@@ -170,7 +170,7 @@ def handle_popup(p: Page) -> None:
def test_dialog_event_should_work_with_inline_script_tag(
page: Page, server: Server
) -> None:
- def handle_route(request: HttpRequestWithPostBody) -> None:
+ def handle_route(request: TestServerRequest) -> None:
request.setHeader("content-type", "text/html")
request.write(b"""""")
request.finish()
diff --git a/tests/sync/test_page_request_intercept.py b/tests/sync/test_page_request_intercept.py
index d62cc5f79..86cf21b63 100644
--- a/tests/sync/test_page_request_intercept.py
+++ b/tests/sync/test_page_request_intercept.py
@@ -15,13 +15,13 @@
import pytest
from playwright.sync_api import Error, Page, Route
-from tests.server import HttpRequestWithPostBody, Server
+from tests.server import Server, TestServerRequest
def test_should_support_timeout_option_in_route_fetch(
server: Server, page: Page
) -> None:
- def _handle(request: HttpRequestWithPostBody) -> None:
+ def _handle(request: TestServerRequest) -> None:
request.responseHeaders.addRawHeader("Content-Length", "4096")
request.responseHeaders.addRawHeader("Content-Type", "text/html")
request.write(b"")