From c62038eb80e76e4b2e09a617faef0f04885fc306 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 15 Sep 2021 18:10:35 +0200 Subject: [PATCH 1/2] feat(roll): roll Playwright 1.15.0-next-1631655106000 --- CONTRIBUTING.md | 7 ++ README.md | 4 +- playwright/_impl/_api_structures.py | 11 ++- playwright/_impl/_browser_type.py | 23 ++--- playwright/_impl/_helper.py | 22 +---- playwright/_impl/_input.py | 3 + playwright/_impl/_network.py | 106 ++++++++++++++--------- playwright/async_api/_generated.py | 130 +++++++++++++++++++++++----- playwright/sync_api/_generated.py | 126 ++++++++++++++++++++++----- scripts/generate_api.py | 2 +- setup.py | 2 +- tests/async/test_input.py | 46 ++++++++++ tests/async/test_interception.py | 2 +- tests/async/test_network.py | 41 ++++++--- 14 files changed, 388 insertions(+), 137 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0a05ae00..a1a7c08c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,13 @@ pre-commit run --all-files For more details look at the [CI configuration](./blob/master/.github/workflows/ci.yml). +Collect coverage + +```sh +pytest --browser chromium --cov-report html --cov=playwright +open htmlcov/index.html +``` + ### Regenerating APIs ```bash diff --git a/README.md b/README.md index bbb400067..7e68ef8cc 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 95.0.4636.0 | ✅ | ✅ | ✅ | +| Chromium 96.0.4641.0 | ✅ | ✅ | ✅ | | WebKit 15.0 | ✅ | ✅ | ✅ | -| Firefox 91.0 | ✅ | ✅ | ✅ | +| Firefox 92.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 05aedd081..ed43a89ff 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -13,7 +13,7 @@ # limitations under the License. import sys -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union if sys.version_info >= (3, 8): # pragma: no cover from typing import Literal, TypedDict @@ -139,3 +139,12 @@ class SecurityDetails(TypedDict): subjectName: Optional[str] validFrom: Optional[float] validTo: Optional[float] + + +class NameValue(TypedDict): + name: str + value: str + + +HeadersArray = List[NameValue] +Headers = Dict[str, str] diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 625708ecc..bc5a22d46 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -38,7 +38,6 @@ ForcedColors, ReducedMotion, locals_to_params, - not_installed_error, ) from playwright._impl._transport import WebSocketTransport from playwright._impl._wait_helper import throw_on_timeout @@ -86,12 +85,7 @@ async def launch( ) -> Browser: params = locals_to_params(locals()) normalize_launch_params(params) - try: - return from_channel(await self._channel.send("launch", params)) - except Exception as e: - if "npx playwright install" in str(e): - raise not_installed_error(f'"{self.name}" browser was not found.') - raise e + return from_channel(await self._channel.send("launch", params)) async def launch_persistent_context( self, @@ -144,16 +138,11 @@ async def launch_persistent_context( params = locals_to_params(locals()) await normalize_context_params(self._connection._is_sync, params) normalize_launch_params(params) - try: - context = from_channel( - await self._channel.send("launchPersistentContext", params) - ) - context._options = params - return context - except Exception as e: - if "npx playwright install" in str(e): - raise not_installed_error(f'"{self.name}" browser was not found.') - raise e + context = from_channel( + await self._channel.send("launchPersistentContext", params) + ) + context._options = params + return context async def connect_over_cdp( self, diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 339231bf2..44e311e79 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -36,6 +36,7 @@ ) from urllib.parse import urljoin +from playwright._impl._api_structures import NameValue from playwright._impl._api_types import Error, TimeoutError if sys.version_info >= (3, 8): # pragma: no cover @@ -69,15 +70,10 @@ class ErrorPayload(TypedDict, total=False): value: Any -class Header(TypedDict): - name: str - value: str - - class ContinueParameters(TypedDict, total=False): url: Optional[str] method: Optional[str] - headers: Optional[List[Header]] + headers: Optional[List[NameValue]] postData: Optional[str] @@ -234,20 +230,6 @@ def is_safe_close_error(error: Exception) -> bool: ) -def not_installed_error(message: str) -> Exception: - return Error( - f""" -================================================================================ -{message} -Please complete Playwright installation via running - - "python -m playwright install" - -================================================================================ -""" - ) - - to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") diff --git a/playwright/_impl/_input.py b/playwright/_impl/_input.py index ba246757f..a97ba5d11 100644 --- a/playwright/_impl/_input.py +++ b/playwright/_impl/_input.py @@ -80,6 +80,9 @@ async def dblclick( ) -> None: await self.click(x, y, delay=delay, button=button, clickCount=2) + async def wheel(self, deltaX: float, deltaY: float) -> None: + await self._channel.send("mouseWheel", locals_to_params(locals())) + class Touchscreen: def __init__(self, channel: Channel) -> None: diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index b971a627b..fa775450d 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -16,12 +16,15 @@ import base64 import json import mimetypes +from collections import defaultdict from pathlib import Path from types import SimpleNamespace from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast from urllib import parse from playwright._impl._api_structures import ( + Headers, + HeadersArray, RemoteAddr, RequestSizes, ResourceTiming, @@ -34,7 +37,7 @@ from_nullable_channel, ) from playwright._impl._event_context_manager import EventContextManagerImpl -from playwright._impl._helper import ContinueParameters, Header, locals_to_params +from playwright._impl._helper import ContinueParameters, locals_to_params from playwright._impl._wait_helper import WaitHelper if TYPE_CHECKING: # pragma: no cover @@ -64,8 +67,8 @@ def __init__( "responseStart": -1, "responseEnd": -1, } - self._headers: List[Header] = self._initializer["headers"] - self._all_headers_future: Optional[asyncio.Future[List[Header]]] = None + self._provisional_headers = RawHeaders(self._initializer["headers"]) + self._all_headers_future: Optional[asyncio.Future[RawHeaders]] = None def __repr__(self) -> str: return f"" @@ -115,10 +118,6 @@ def post_data_buffer(self) -> Optional[bytes]: return None return base64.b64decode(b64_content) - @property - def headers(self) -> Dict[str, str]: - return headers_array_to_object(self._headers, True) - async def response(self) -> Optional["Response"]: return from_nullable_channel(await self._channel.send("response")) @@ -145,25 +144,27 @@ def failure(self) -> Optional[str]: def timing(self) -> ResourceTiming: return self._timing - async def all_headers(self) -> Dict[str, str]: - return headers_array_to_object(await self._get_headers_if_needed(), True) + @property + def headers(self) -> Headers: + return self._provisional_headers.headers() - async def headers_array(self) -> List[List[str]]: - return list( - map( - lambda header: [header["name"], header["value"]], - await self._get_headers_if_needed(), - ) - ) + async def all_headers(self) -> Headers: + return (await self._actual_headers()).headers() + + async def headers_array(self) -> HeadersArray: + return (await self._actual_headers()).headers_array() + + async def header_value(self, name: str) -> Optional[str]: + return (await self._actual_headers()).get(name) - async def _get_headers_if_needed(self) -> List[Header]: + async def _actual_headers(self) -> "RawHeaders": if not self._all_headers_future: self._all_headers_future = asyncio.Future() response = await self.response() if not response: - return self._headers + return self._provisional_headers headers = await response._channel.send("rawRequestHeaders") - self._all_headers_future.set_result(headers) + self._all_headers_future.set_result(RawHeaders(headers)) return await self._all_headers_future @@ -256,10 +257,10 @@ def __init__( self._request._timing["connectEnd"] = timing["connectEnd"] self._request._timing["requestStart"] = timing["requestStart"] self._request._timing["responseStart"] = timing["responseStart"] - self._headers = headers_array_to_object( - cast(List[Header], self._initializer["headers"]), True + self._provisional_headers = RawHeaders( + cast(HeadersArray, self._initializer["headers"]) ) - self._raw_headers_future: Optional[asyncio.Future[List[Header]]] = None + self._raw_headers_future: Optional[asyncio.Future[RawHeaders]] = None self._finished_future: asyncio.Future[bool] = asyncio.Future() def __repr__(self) -> str: @@ -284,25 +285,26 @@ def status_text(self) -> str: return self._initializer["statusText"] @property - def headers(self) -> Dict[str, str]: - return self._headers.copy() + def headers(self) -> Headers: + return self._provisional_headers.headers() - async def all_headers(self) -> Dict[str, str]: - return headers_array_to_object(await self._get_headers_if_needed(), True) + async def all_headers(self) -> Headers: + return (await self._actual_headers()).headers() - async def headers_array(self) -> List[List[str]]: - return list( - map( - lambda header: [header["name"], header["value"]], - await self._get_headers_if_needed(), - ) - ) + async def headers_array(self) -> HeadersArray: + return (await self._actual_headers()).headers_array() + + async def header_value(self, name: str) -> Optional[str]: + return (await self._actual_headers()).get(name) + + async def header_values(self, name: str) -> List[str]: + return (await self._actual_headers()).get_all(name) - async def _get_headers_if_needed(self) -> List[Header]: + async def _actual_headers(self) -> "RawHeaders": if not self._raw_headers_future: self._raw_headers_future = asyncio.Future() - headers = cast(List[Header], await self._channel.send("rawResponseHeaders")) - self._raw_headers_future.set_result(headers) + headers = cast(HeadersArray, await self._channel.send("rawResponseHeaders")) + self._raw_headers_future.set_result(RawHeaders(headers)) return await self._raw_headers_future async def server_addr(self) -> Optional[RemoteAddr]: @@ -420,12 +422,32 @@ def _on_close(self) -> None: self.emit(WebSocket.Events.Close) -def serialize_headers(headers: Dict[str, str]) -> List[Header]: +def serialize_headers(headers: Dict[str, str]) -> HeadersArray: return [{"name": name, "value": value} for name, value in headers.items()] -def headers_array_to_object(headers: List[Header], lower_case: bool) -> Dict[str, str]: - return { - (header["name"].lower() if lower_case else header["name"]): header["value"] - for header in headers - } +class RawHeaders: + def __init__(self, headers: HeadersArray) -> None: + self._headers_array = headers + self._headers_map = defaultdict(list) + for header in headers: + self._headers_map[header["name"].lower()].append(header["value"]) + + def get(self, name: str) -> Optional[str]: + values = self.get_all(name) + if not values: + return None + separator = "\n" if name.lower() == "set-cookie" else ", " + return separator.join(values) + + def get_all(self, name: str) -> List[str]: + return self._headers_map[name.lower()].copy() + + def headers(self) -> Dict[str, str]: + result = {} + for name in self._headers_map.keys(): + result[name] = cast(str, self.get(name)) + return result + + def headers_array(self) -> HeadersArray: + return self._headers_array diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 20097f446..e6a347e91 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -29,6 +29,7 @@ FloatRect, Geolocation, HttpCredentials, + NameValue, PdfMargins, Position, ProxySettings, @@ -154,18 +155,6 @@ def post_data_buffer(self) -> typing.Optional[bytes]: """ return mapping.from_maybe_impl(self._impl_obj.post_data_buffer) - @property - def headers(self) -> typing.Dict[str, str]: - """Request.headers - - **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead. - - Returns - ------- - Dict[str, str] - """ - return mapping.from_maybe_impl(self._impl_obj.headers) - @property def frame(self) -> "Frame": """Request.frame @@ -265,6 +254,18 @@ def timing(self) -> ResourceTiming: """ return mapping.from_impl(self._impl_obj.timing) + @property + def headers(self) -> typing.Dict[str, str]: + """Request.headers + + **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead. + + Returns + ------- + Dict[str, str] + """ + return mapping.from_maybe_impl(self._impl_obj.headers) + async def sizes(self) -> RequestSizes: """Request.sizes @@ -319,21 +320,42 @@ async def all_headers(self) -> typing.Dict[str, str]: await self._async("request.all_headers", self._impl_obj.all_headers()) ) - async def headers_array(self) -> typing.List[typing.List[str]]: + async def headers_array(self) -> typing.List[NameValue]: """Request.headers_array An array with all the request HTTP headers associated with this request. Unlike `request.all_headers()`, header - names are not lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + names are NOT lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. Returns ------- - List[List[str]] + List[{name: str, value: str}] """ - return mapping.from_maybe_impl( + return mapping.from_impl_list( await self._async("request.headers_array", self._impl_obj.headers_array()) ) + async def header_value(self, name: str) -> typing.Optional[str]: + """Request.header_value + + Returns the value of the header matching the name. The name is case insensitive. + + Parameters + ---------- + name : str + Name of the header. + + Returns + ------- + Union[str, NoneType] + """ + + return mapping.from_maybe_impl( + await self._async( + "request.header_value", self._impl_obj.header_value(name=name) + ) + ) + mapping.register(RequestImpl, Request) @@ -437,21 +459,65 @@ async def all_headers(self) -> typing.Dict[str, str]: await self._async("response.all_headers", self._impl_obj.all_headers()) ) - async def headers_array(self) -> typing.List[typing.List[str]]: + async def headers_array(self) -> typing.List[NameValue]: """Response.headers_array An array with all the request HTTP headers associated with this response. Unlike `response.all_headers()`, header - names are not lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + names are NOT lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. Returns ------- - List[List[str]] + List[{name: str, value: str}] """ - return mapping.from_maybe_impl( + return mapping.from_impl_list( await self._async("response.headers_array", self._impl_obj.headers_array()) ) + async def header_value(self, name: str) -> typing.Optional[str]: + """Response.header_value + + Returns the value of the header matching the name. The name is case insensitive. If multiple headers have the same name + (except `set-cookie`), they are returned as a list separated by `, `. For `set-cookie`, the `\\n` separator is used. If + no headers are found, `null` is returned. + + Parameters + ---------- + name : str + Name of the header. + + Returns + ------- + Union[str, NoneType] + """ + + return mapping.from_maybe_impl( + await self._async( + "response.header_value", self._impl_obj.header_value(name=name) + ) + ) + + async def header_values(self, name: str) -> typing.List[str]: + """Response.header_values + + Returns all values of the headers matching the name, for example `set-cookie`. The name is case insensitive. + + Parameters + ---------- + name : str + Name of the header. + + Returns + ------- + List[str] + """ + + return mapping.from_maybe_impl( + await self._async( + "response.header_values", self._impl_obj.header_values(name=name) + ) + ) + async def server_addr(self) -> typing.Optional[RemoteAddr]: """Response.server_addr @@ -485,7 +551,7 @@ async def security_details(self) -> typing.Optional[SecurityDetails]: async def finished(self) -> NoneType: """Response.finished - Waits for this response to finish, returns failure error if request failed. + Waits for this response to finish, returns always `null`. """ return mapping.from_maybe_impl( @@ -1170,6 +1236,28 @@ async def dblclick( ) ) + async def wheel(self, delta_x: float, delta_y: float) -> NoneType: + """Mouse.wheel + + Dispatches a `wheel` event. + + > NOTE: Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to + finish before returning. + + Parameters + ---------- + delta_x : float + Pixels to scroll horizontally. + delta_y : float + Pixels to scroll vertically. + """ + + return mapping.from_maybe_impl( + await self._async( + "mouse.wheel", self._impl_obj.wheel(deltaX=delta_x, deltaY=delta_y) + ) + ) + mapping.register(MouseImpl, Mouse) diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index f77db5ef6..6edc4a557 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -29,6 +29,7 @@ FloatRect, Geolocation, HttpCredentials, + NameValue, PdfMargins, Position, ProxySettings, @@ -154,18 +155,6 @@ def post_data_buffer(self) -> typing.Optional[bytes]: """ return mapping.from_maybe_impl(self._impl_obj.post_data_buffer) - @property - def headers(self) -> typing.Dict[str, str]: - """Request.headers - - **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead. - - Returns - ------- - Dict[str, str] - """ - return mapping.from_maybe_impl(self._impl_obj.headers) - @property def frame(self) -> "Frame": """Request.frame @@ -265,6 +254,18 @@ def timing(self) -> ResourceTiming: """ return mapping.from_impl(self._impl_obj.timing) + @property + def headers(self) -> typing.Dict[str, str]: + """Request.headers + + **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead. + + Returns + ------- + Dict[str, str] + """ + return mapping.from_maybe_impl(self._impl_obj.headers) + def sizes(self) -> RequestSizes: """Request.sizes @@ -317,21 +318,40 @@ def all_headers(self) -> typing.Dict[str, str]: self._sync("request.all_headers", self._impl_obj.all_headers()) ) - def headers_array(self) -> typing.List[typing.List[str]]: + def headers_array(self) -> typing.List[NameValue]: """Request.headers_array An array with all the request HTTP headers associated with this request. Unlike `request.all_headers()`, header - names are not lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + names are NOT lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. Returns ------- - List[List[str]] + List[{name: str, value: str}] """ - return mapping.from_maybe_impl( + return mapping.from_impl_list( self._sync("request.headers_array", self._impl_obj.headers_array()) ) + def header_value(self, name: str) -> typing.Optional[str]: + """Request.header_value + + Returns the value of the header matching the name. The name is case insensitive. + + Parameters + ---------- + name : str + Name of the header. + + Returns + ------- + Union[str, NoneType] + """ + + return mapping.from_maybe_impl( + self._sync("request.header_value", self._impl_obj.header_value(name=name)) + ) + mapping.register(RequestImpl, Request) @@ -435,21 +455,63 @@ def all_headers(self) -> typing.Dict[str, str]: self._sync("response.all_headers", self._impl_obj.all_headers()) ) - def headers_array(self) -> typing.List[typing.List[str]]: + def headers_array(self) -> typing.List[NameValue]: """Response.headers_array An array with all the request HTTP headers associated with this response. Unlike `response.all_headers()`, header - names are not lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + names are NOT lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. Returns ------- - List[List[str]] + List[{name: str, value: str}] """ - return mapping.from_maybe_impl( + return mapping.from_impl_list( self._sync("response.headers_array", self._impl_obj.headers_array()) ) + def header_value(self, name: str) -> typing.Optional[str]: + """Response.header_value + + Returns the value of the header matching the name. The name is case insensitive. If multiple headers have the same name + (except `set-cookie`), they are returned as a list separated by `, `. For `set-cookie`, the `\\n` separator is used. If + no headers are found, `null` is returned. + + Parameters + ---------- + name : str + Name of the header. + + Returns + ------- + Union[str, NoneType] + """ + + return mapping.from_maybe_impl( + self._sync("response.header_value", self._impl_obj.header_value(name=name)) + ) + + def header_values(self, name: str) -> typing.List[str]: + """Response.header_values + + Returns all values of the headers matching the name, for example `set-cookie`. The name is case insensitive. + + Parameters + ---------- + name : str + Name of the header. + + Returns + ------- + List[str] + """ + + return mapping.from_maybe_impl( + self._sync( + "response.header_values", self._impl_obj.header_values(name=name) + ) + ) + def server_addr(self) -> typing.Optional[RemoteAddr]: """Response.server_addr @@ -481,7 +543,7 @@ def security_details(self) -> typing.Optional[SecurityDetails]: def finished(self) -> NoneType: """Response.finished - Waits for this response to finish, returns failure error if request failed. + Waits for this response to finish, returns always `null`. """ return mapping.from_maybe_impl( @@ -1132,6 +1194,28 @@ def dblclick( ) ) + def wheel(self, delta_x: float, delta_y: float) -> NoneType: + """Mouse.wheel + + Dispatches a `wheel` event. + + > NOTE: Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to + finish before returning. + + Parameters + ---------- + delta_x : float + Pixels to scroll horizontally. + delta_y : float + Pixels to scroll vertically. + """ + + return mapping.from_maybe_impl( + self._sync( + "mouse.wheel", self._impl_obj.wheel(deltaX=delta_x, deltaY=delta_y) + ) + ) + mapping.register(MouseImpl, Mouse) diff --git a/scripts/generate_api.py b/scripts/generate_api.py index 3b1852d21..8566ff520 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -217,7 +217,7 @@ def return_value(value: Any) -> List[str]: from playwright._impl._accessibility import Accessibility as AccessibilityImpl -from playwright._impl._api_structures import Cookie, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes +from playwright._impl._api_structures import Cookie, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue from playwright._impl._browser import Browser as BrowserImpl from playwright._impl._browser_context import BrowserContext as BrowserContextImpl from playwright._impl._browser_type import BrowserType as BrowserTypeImpl diff --git a/setup.py b/setup.py index 8550e051d..ec2ca9a57 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.15.0-next-1631205294000" +driver_version = "1.15.0-next-1631655106000" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_input.py b/tests/async/test_input.py index 88b026b49..3ff92ebe6 100644 --- a/tests/async/test_input.py +++ b/tests/async/test_input.py @@ -238,3 +238,49 @@ async def test_should_work_for_webkitdirectory(page): await page.click("input") file_chooser = await fc_info.value assert file_chooser.is_multiple() + + +async def test_wheel_should_work(page: Page, server): + await page.set_content( + """ +
+ """ + ) + await page.mouse.move(50, 60) + await _listen_for_wheel_events(page, "div") + await page.mouse.wheel(0, 100) + assert await page.evaluate("window.lastEvent") == { + "deltaX": 0, + "deltaY": 100, + "clientX": 50, + "clientY": 60, + "deltaMode": 0, + "ctrlKey": False, + "shiftKey": False, + "altKey": False, + "metaKey": False, + } + await page.wait_for_function("window.scrollY === 100") + + +async def _listen_for_wheel_events(page: Page, selector: str) -> None: + await page.evaluate( + """ + selector => { + document.querySelector(selector).addEventListener('wheel', (e) => { + window['lastEvent'] = { + deltaX: e.deltaX, + deltaY: e.deltaY, + clientX: e.clientX, + clientY: e.clientY, + deltaMode: e.deltaMode, + ctrlKey: e.ctrlKey, + shiftKey: e.shiftKey, + altKey: e.altKey, + metaKey: e.metaKey, + }; + }, { passive: false }); + } + """, + selector, + ) diff --git a/tests/async/test_interception.py b/tests/async/test_interception.py index ea6643b43..3fc8f990f 100644 --- a/tests/async/test_interception.py +++ b/tests/async/test_interception.py @@ -204,7 +204,7 @@ async def test_page_route_should_work_with_redirect_inside_sync_XHR(page, server async def test_page_route_should_work_with_custom_referer_headers(page, server): - await page.set_extra_http_headers({"referer": server.EMPTY_PAGE}) + await page.set_extra_http_headers({"Referer": server.EMPTY_PAGE}) def assert_headers(route): assert route.request.headers["referer"] == server.EMPTY_PAGE diff --git a/tests/async/test_network.py b/tests/async/test_network.py index 0009615ea..2d94c2203 100644 --- a/tests/async/test_network.py +++ b/tests/async/test_network.py @@ -239,9 +239,11 @@ async def test_should_report_request_headers_array( expected_headers = [] def handle(request: http.Request): - for key, values in request.requestHeaders.getAllRawHeaders(): + for name, values in request.requestHeaders.getAllRawHeaders(): for value in values: - expected_headers.append([key.decode().lower(), value.decode()]) + expected_headers.append( + {"name": name.decode().lower(), "value": value.decode()} + ) request.finish() server.set_route("/headers", handle) @@ -259,11 +261,24 @@ def handle(request: http.Request): """ ) request = await request_info.value - assert sorted( + sorted_pw_request_headers = sorted( list( - map(lambda item: [item[0].lower(), item[1]], await request.headers_array()) - ) - ) == sorted(expected_headers) + map( + lambda header: { + "name": header["name"].lower(), + "value": header["value"], + }, + await request.headers_array(), + ) + ), + key=lambda header: header["name"], + ) + sorted_expected_headers = sorted( + expected_headers, key=lambda header: header["name"] + ) + assert sorted_pw_request_headers == sorted_expected_headers + assert await request.header_value("Header-A") == "value-a, value-a-1, value-a-2" + assert await request.header_value("not-there") is None async def test_should_report_response_headers_array( @@ -272,8 +287,9 @@ async def test_should_report_response_headers_array( if is_win and browser_name == "webkit": pytest.skip("libcurl does not support non-set-cookie multivalue headers") expected_headers = { - "Header-A": ["value-a", "value-a-1", "value-a-2"], - "Header-B": ["value-b"], + "header-a": ["value-a", "value-a-1", "value-a-2"], + "header-b": ["value-b"], + "set-cookie": ["a=b", "c=d"], } def handle(request: http.Request): @@ -290,9 +306,10 @@ def handle(request: http.Request): """ ) response = await response_info.value - headers = await response.headers_array() actual_headers = {} - for name, value in headers: + for header in await response.headers_array(): + name = header["name"].lower() + value = header["value"] if not actual_headers.get(name): actual_headers[name] = [] actual_headers[name].append(value) @@ -303,6 +320,10 @@ def handle(request: http.Request): if key.lower() in actual_headers: actual_headers.pop(key.lower()) assert actual_headers == expected_headers + assert await response.header_value("not-there") is None + assert await response.header_value("set-cookie") == "a=b\nc=d" + assert await response.header_value("header-a") == "value-a, value-a-1, value-a-2" + assert await response.header_values("set-cookie") == ["a=b", "c=d"] async def test_response_headers_should_work(page: Page, server): From 1fb936b9c48687cfaa2bfc3f0a87c341bd9907f2 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Wed, 15 Sep 2021 20:47:51 +0200 Subject: [PATCH 2/2] fix it --- playwright/_impl/_network.py | 6 +++--- tests/async/test_interception.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index fa775450d..1666f5c3e 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -429,9 +429,9 @@ def serialize_headers(headers: Dict[str, str]) -> HeadersArray: class RawHeaders: def __init__(self, headers: HeadersArray) -> None: self._headers_array = headers - self._headers_map = defaultdict(list) + self._headers_map: Dict[str, Dict[str, bool]] = defaultdict(dict) for header in headers: - self._headers_map[header["name"].lower()].append(header["value"]) + self._headers_map[header["name"].lower()][header["value"]] = True def get(self, name: str) -> Optional[str]: values = self.get_all(name) @@ -441,7 +441,7 @@ def get(self, name: str) -> Optional[str]: return separator.join(values) def get_all(self, name: str) -> List[str]: - return self._headers_map[name.lower()].copy() + return list(self._headers_map[name.lower()].keys()) def headers(self) -> Dict[str, str]: result = {} diff --git a/tests/async/test_interception.py b/tests/async/test_interception.py index 3fc8f990f..ea6643b43 100644 --- a/tests/async/test_interception.py +++ b/tests/async/test_interception.py @@ -204,7 +204,7 @@ async def test_page_route_should_work_with_redirect_inside_sync_XHR(page, server async def test_page_route_should_work_with_custom_referer_headers(page, server): - await page.set_extra_http_headers({"Referer": server.EMPTY_PAGE}) + await page.set_extra_http_headers({"referer": server.EMPTY_PAGE}) def assert_headers(route): assert route.request.headers["referer"] == server.EMPTY_PAGE