From 8047115333d095308362ab8935fbdf8309ebcfce Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 21 Nov 2023 16:20:26 +0100 Subject: [PATCH] chore(roll): roll Playwright to 1.40.0 --- README.md | 4 +- playwright/_impl/_assertions.py | 5 +- playwright/_impl/_browser_type.py | 1 + playwright/_impl/_element_handle.py | 35 +++---- playwright/_impl/_frame.py | 25 +++-- playwright/_impl/_set_input_files_helpers.py | 102 ++++++++----------- playwright/async_api/_generated.py | 59 ++++++++--- playwright/sync_api/_generated.py | 59 ++++++++--- setup.py | 2 +- tests/assets/file-to-upload-2.txt | 1 + tests/async/test_assertions.py | 9 ++ tests/async/test_browsertype_connect.py | 28 +++++ tests/async/test_evaluate.py | 2 +- tests/async/test_input.py | 21 ++++ tests/sync/test_assertions.py | 9 ++ tests/sync/test_browsertype_connect.py | 30 +++++- tests/sync/test_input.py | 21 ++++ 17 files changed, 287 insertions(+), 126 deletions(-) create mode 100644 tests/assets/file-to-upload-2.txt diff --git a/README.md b/README.md index 6191d65358..fc53802879 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 119.0.6045.21 | ✅ | ✅ | ✅ | +| Chromium 120.0.6099.28 | ✅ | ✅ | ✅ | | WebKit 17.4 | ✅ | ✅ | ✅ | -| Firefox 118.0.1 | ✅ | ✅ | ✅ | +| Firefox 119.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 730e1e294e..cd60f1679e 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -215,10 +215,11 @@ async def to_have_attribute( self, name: str, value: Union[str, Pattern[str]], + ignore_case: bool = None, timeout: float = None, ) -> None: __tracebackhide__ = True - expected_text = to_expected_text_values([value]) + expected_text = to_expected_text_values([value], ignore_case=ignore_case) await self._expect_impl( "to.have.attribute.value", FrameExpectOptions( @@ -235,7 +236,7 @@ async def not_to_have_attribute( timeout: float = None, ) -> None: __tracebackhide__ = True - await self._not.to_have_attribute(name, value, timeout) + await self._not.to_have_attribute(name, value, timeout=timeout) async def to_have_class( self, diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 4a916171ab..6e712f4aa5 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -137,6 +137,7 @@ async def launch_persistent_context( acceptDownloads: bool = None, tracesDir: Union[pathlib.Path, str] = None, chromiumSandbox: bool = None, + firefoxUserPrefs: Dict[str, Union[str, float, bool]] = None, recordHarPath: Union[Path, str] = None, recordHarOmitContent: bool = None, recordVideoDir: Union[Path, str] = None, diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index 3b96c444e9..9d98751936 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -18,7 +18,11 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast from playwright._impl._api_structures import FilePayload, FloatRect, Position -from playwright._impl._connection import ChannelOwner, from_nullable_channel +from playwright._impl._connection import ( + ChannelOwner, + filter_none, + from_nullable_channel, +) from playwright._impl._helper import ( Error, KeyboardModifier, @@ -191,20 +195,17 @@ async def set_input_files( timeout: float = None, noWaitAfter: bool = None, ) -> None: - params = locals_to_params(locals()) frame = await self.owner_frame() if not frame: raise Error("Cannot set input files to detached element") converted = await convert_input_files(files, frame.page.context) - if converted["files"] is not None: - await self._channel.send( - "setInputFiles", {**params, "files": converted["files"]} - ) - else: - await self._channel.send( - "setInputFilePaths", - locals_to_params({**params, **converted, "files": None}), - ) + await self._channel.send( + "setInputFiles", + { + **filter_none({"timeout": timeout, "noWaitAfter": noWaitAfter}), + **converted, + }, + ) async def focus(self) -> None: await self._channel.send("focus") @@ -407,14 +408,4 @@ def convert_select_option_values( element = [element] elements = list(map(lambda e: e._channel, element)) - return filter_out_none(dict(options=options, elements=elements)) - - -def filter_out_none(args: Dict) -> Any: - copy = {} - for key in args: - if key == "self": - continue - if args[key] is not None: - copy[key] = args[key] - return copy + return filter_none(dict(options=options, elements=elements)) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index d8836e3bb2..124409681f 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -22,6 +22,7 @@ from playwright._impl._api_structures import AriaRole, FilePayload, Position from playwright._impl._connection import ( ChannelOwner, + filter_none, from_channel, from_nullable_channel, ) @@ -689,17 +690,21 @@ async def set_input_files( timeout: float = None, noWaitAfter: bool = None, ) -> None: - params = locals_to_params(locals()) converted = await convert_input_files(files, self.page.context) - if converted["files"] is not None: - await self._channel.send( - "setInputFiles", {**params, "files": converted["files"]} - ) - else: - await self._channel.send( - "setInputFilePaths", - locals_to_params({**params, **converted, "files": None}), - ) + await self._channel.send( + "setInputFiles", + { + **filter_none( + { + "selector": selector, + "strict": strict, + "timeout": timeout, + "noWaitAfter": noWaitAfter, + } + ), + **converted, + }, + ) async def type( self, diff --git a/playwright/_impl/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py index 8aabc4c612..b1e9292521 100644 --- a/playwright/_impl/_set_input_files_helpers.py +++ b/playwright/_impl/_set_input_files_helpers.py @@ -15,7 +15,7 @@ import os import sys from pathlib import Path -from typing import TYPE_CHECKING, List, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast if sys.version_info >= (3, 8): # pragma: no cover from typing import TypedDict @@ -23,7 +23,7 @@ from typing_extensions import TypedDict from playwright._impl._connection import Channel, from_channel -from playwright._impl._helper import Error, async_readfile +from playwright._impl._helper import Error from playwright._impl._writable_stream import WritableStream if TYPE_CHECKING: # pragma: no cover @@ -34,81 +34,61 @@ SIZE_LIMIT_IN_BYTES = 50 * 1024 * 1024 -class InputFilesList(TypedDict): +class InputFilesList(TypedDict, total=False): streams: Optional[List[Channel]] localPaths: Optional[List[str]] - files: Optional[List[FilePayload]] + payloads: Optional[List[Dict[str, Union[str, bytes]]]] async def convert_input_files( files: Union[str, Path, FilePayload, List[Union[str, Path]], List[FilePayload]], context: "BrowserContext", ) -> InputFilesList: - file_list = files if isinstance(files, list) else [files] + items = files if isinstance(files, list) else [files] - total_buffer_size_exceeds_limit = ( - sum( - [ - len(f.get("buffer", "")) - for f in file_list - if not isinstance(f, (str, Path)) - ] - ) - > SIZE_LIMIT_IN_BYTES - ) - if total_buffer_size_exceeds_limit: - raise Error( - "Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead." - ) - - total_file_size_exceeds_limit = ( - sum([os.stat(f).st_size for f in file_list if isinstance(f, (str, Path))]) - > SIZE_LIMIT_IN_BYTES - ) - if total_file_size_exceeds_limit: + if any([isinstance(item, (str, Path)) for item in items]): + if not all([isinstance(item, (str, Path)) for item in items]): + raise Error("File paths cannot be mixed with buffers") if context._channel._connection.is_remote: streams = [] - for file in file_list: - assert isinstance(file, (str, Path)) + for item in items: + assert isinstance(item, (str, Path)) + last_modified_ms = int(os.path.getmtime(item) * 1000) stream: WritableStream = from_channel( await context._channel.send( - "createTempFile", {"name": os.path.basename(file)} + "createTempFile", + { + "name": os.path.basename(item), + "lastModifiedMs": last_modified_ms, + }, ) ) - await stream.copy(file) + await stream.copy(item) streams.append(stream._channel) - return InputFilesList(streams=streams, localPaths=None, files=None) - local_paths = [] - for p in file_list: - assert isinstance(p, (str, Path)) - local_paths.append(str(Path(p).absolute().resolve())) - return InputFilesList(streams=None, localPaths=local_paths, files=None) + return InputFilesList(streams=streams) + return InputFilesList( + localPaths=[ + str(Path(cast(Union[str, Path], item)).absolute().resolve()) + for item in items + ] + ) - return InputFilesList( - streams=None, localPaths=None, files=await _normalize_file_payloads(files) + file_payload_exceeds_size_limit = ( + sum([len(f.get("buffer", "")) for f in items if not isinstance(f, (str, Path))]) + > SIZE_LIMIT_IN_BYTES ) + if file_payload_exceeds_size_limit: + raise Error( + "Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead." + ) - -async def _normalize_file_payloads( - files: Union[str, Path, FilePayload, List[Union[str, Path]], List[FilePayload]] -) -> List: - file_list = files if isinstance(files, list) else [files] - file_payloads: List = [] - for item in file_list: - if isinstance(item, (str, Path)): - file_payloads.append( - { - "name": os.path.basename(item), - "buffer": base64.b64encode(await async_readfile(item)).decode(), - } - ) - else: - file_payloads.append( - { - "name": item["name"], - "mimeType": item["mimeType"], - "buffer": base64.b64encode(item["buffer"]).decode(), - } - ) - - return file_payloads + return InputFilesList( + payloads=[ + { + "name": item["name"], + "mimeType": item["mimeType"], + "buffer": base64.b64encode(item["buffer"]).decode(), + } + for item in cast(List[FilePayload], items) + ] + ) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 426c248227..eba7560d23 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -1920,13 +1920,16 @@ async def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -4102,13 +4105,16 @@ async def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -7244,8 +7250,8 @@ async def failure(self) -> typing.Optional[str]: async def path(self) -> pathlib.Path: """Download.path - Returns path to the downloaded file in case of successful download. The method will wait for the download to finish - if necessary. The method throws when connected remotely. + Returns path to the downloaded file for a successful download, or throws for a failed/canceled download. The method + will wait for the download to finish if necessary. The method throws when connected remotely. Note that the download's file name is a random GUID, use `download.suggested_filename()` to get suggested file name. @@ -8543,13 +8549,16 @@ async def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -9025,7 +9034,7 @@ async def expose_binding( async def run(playwright: Playwright): webkit = playwright.webkit - browser = await webkit.launch(headless=false) + browser = await webkit.launch(headless=False) context = await browser.new_context() page = await context.new_page() await page.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) @@ -9051,7 +9060,7 @@ async def main(): def run(playwright: Playwright): webkit = playwright.webkit - browser = webkit.launch(headless=false) + browser = webkit.launch(headless=False) context = browser.new_context() page = context.new_page() page.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) @@ -13310,7 +13319,7 @@ async def expose_binding( async def run(playwright: Playwright): webkit = playwright.webkit - browser = await webkit.launch(headless=false) + browser = await webkit.launch(headless=False) context = await browser.new_context() await context.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) page = await context.new_page() @@ -13336,7 +13345,7 @@ async def main(): def run(playwright: Playwright): webkit = playwright.webkit - browser = webkit.launch(headless=false) + browser = webkit.launch(headless=False) context = browser.new_context() context.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) page = context.new_page() @@ -14796,6 +14805,9 @@ async def launch_persistent_context( accept_downloads: typing.Optional[bool] = None, traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, chromium_sandbox: typing.Optional[bool] = None, + firefox_user_prefs: typing.Optional[ + typing.Dict[str, typing.Union[str, float, bool]] + ] = None, record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, record_har_omit_content: typing.Optional[bool] = None, record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, @@ -14931,6 +14943,9 @@ async def launch_persistent_context( If specified, traces are saved into this directory. chromium_sandbox : Union[bool, None] Enable Chromium sandboxing. Defaults to `false`. + firefox_user_prefs : Union[Dict[str, Union[bool, float, str]], None] + Firefox user preferences. Learn more about the Firefox user preferences at + [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). record_har_path : Union[pathlib.Path, str, None] Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` @@ -15018,6 +15033,7 @@ async def launch_persistent_context( acceptDownloads=accept_downloads, tracesDir=traces_dir, chromiumSandbox=chromium_sandbox, + firefoxUserPrefs=mapping.to_impl(firefox_user_prefs), recordHarPath=record_har_path, recordHarOmitContent=record_har_omit_content, recordVideoDir=record_video_dir, @@ -15804,13 +15820,16 @@ async def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -18385,8 +18404,8 @@ async def dispose(self) -> None: """APIRequestContext.dispose All responses returned by `a_pi_request_context.get()` and similar methods are stored in the memory, so that - you can later call `a_pi_response.body()`. This method discards all stored responses, and makes - `a_pi_response.body()` throw \"Response disposed\" error. + you can later call `a_pi_response.body()`.This method discards all its resources, calling any method on + disposed `APIRequestContext` will throw an exception. """ return mapping.from_maybe_impl(await self._impl_obj.dispose()) @@ -19277,6 +19296,11 @@ async def to_contain_text( Ensures the `Locator` points to an element that contains the given text. You can use regular expressions for the value as well. + **Details** + + When `expected` parameter is a string, Playwright will normalize whitespaces and line breaks both in the actual + text and in the expected string before matching. When regular expression is used, the actual text is matched as is. + **Usage** ```py @@ -19416,6 +19440,7 @@ async def to_have_attribute( name: str, value: typing.Union[str, typing.Pattern[str]], *, + ignore_case: typing.Optional[bool] = None, timeout: typing.Optional[float] = None ) -> None: """LocatorAssertions.to_have_attribute @@ -19444,6 +19469,9 @@ async def to_have_attribute( Attribute name. value : Union[Pattern[str], str] Expected attribute value. + ignore_case : Union[bool, None] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. timeout : Union[float, None] Time to retry the assertion for in milliseconds. Defaults to `5000`. """ @@ -19451,7 +19479,7 @@ async def to_have_attribute( return mapping.from_maybe_impl( await self._impl_obj.to_have_attribute( - name=name, value=value, timeout=timeout + name=name, value=value, ignore_case=ignore_case, timeout=timeout ) ) @@ -20010,6 +20038,11 @@ async def to_have_text( Ensures the `Locator` points to an element with the given text. You can use regular expressions for the value as well. + **Details** + + When `expected` parameter is a string, Playwright will normalize whitespaces and line breaks both in the actual + text and in the expected string before matching. When regular expression is used, the actual text is matched as is. + **Usage** ```py diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 182d31874c..4059a61493 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -1926,13 +1926,16 @@ def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -4172,13 +4175,16 @@ def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -7364,8 +7370,8 @@ def failure(self) -> typing.Optional[str]: def path(self) -> pathlib.Path: """Download.path - Returns path to the downloaded file in case of successful download. The method will wait for the download to finish - if necessary. The method throws when connected remotely. + Returns path to the downloaded file for a successful download, or throws for a failed/canceled download. The method + will wait for the download to finish if necessary. The method throws when connected remotely. Note that the download's file name is a random GUID, use `download.suggested_filename()` to get suggested file name. @@ -8577,13 +8583,16 @@ def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -9073,7 +9082,7 @@ def expose_binding( async def run(playwright: Playwright): webkit = playwright.webkit - browser = await webkit.launch(headless=false) + browser = await webkit.launch(headless=False) context = await browser.new_context() page = await context.new_page() await page.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) @@ -9099,7 +9108,7 @@ async def main(): def run(playwright: Playwright): webkit = playwright.webkit - browser = webkit.launch(headless=false) + browser = webkit.launch(headless=False) context = browser.new_context() page = context.new_page() page.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) @@ -13360,7 +13369,7 @@ def expose_binding( async def run(playwright: Playwright): webkit = playwright.webkit - browser = await webkit.launch(headless=false) + browser = await webkit.launch(headless=False) context = await browser.new_context() await context.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) page = await context.new_page() @@ -13386,7 +13395,7 @@ async def main(): def run(playwright: Playwright): webkit = playwright.webkit - browser = webkit.launch(headless=false) + browser = webkit.launch(headless=False) context = browser.new_context() context.expose_binding(\"pageURL\", lambda source: source[\"page\"].url) page = context.new_page() @@ -14864,6 +14873,9 @@ def launch_persistent_context( accept_downloads: typing.Optional[bool] = None, traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, chromium_sandbox: typing.Optional[bool] = None, + firefox_user_prefs: typing.Optional[ + typing.Dict[str, typing.Union[str, float, bool]] + ] = None, record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, record_har_omit_content: typing.Optional[bool] = None, record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, @@ -14999,6 +15011,9 @@ def launch_persistent_context( If specified, traces are saved into this directory. chromium_sandbox : Union[bool, None] Enable Chromium sandboxing. Defaults to `false`. + firefox_user_prefs : Union[Dict[str, Union[bool, float, str]], None] + Firefox user preferences. Learn more about the Firefox user preferences at + [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). record_har_path : Union[pathlib.Path, str, None] Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` @@ -15087,6 +15102,7 @@ def launch_persistent_context( acceptDownloads=accept_downloads, tracesDir=traces_dir, chromiumSandbox=chromium_sandbox, + firefoxUserPrefs=mapping.to_impl(firefox_user_prefs), recordHarPath=record_har_path, recordHarOmitContent=record_har_omit_content, recordVideoDir=record_video_dir, @@ -15886,13 +15902,16 @@ def dispatch_event( properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: + - [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent/DeviceMotionEvent) + - [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/DeviceOrientationEvent) - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) + - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) - [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) - [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) - [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) - [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) - - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + - [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/WheelEvent) You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: @@ -18519,8 +18538,8 @@ def dispose(self) -> None: """APIRequestContext.dispose All responses returned by `a_pi_request_context.get()` and similar methods are stored in the memory, so that - you can later call `a_pi_response.body()`. This method discards all stored responses, and makes - `a_pi_response.body()` throw \"Response disposed\" error. + you can later call `a_pi_response.body()`.This method discards all its resources, calling any method on + disposed `APIRequestContext` will throw an exception. """ return mapping.from_maybe_impl(self._sync(self._impl_obj.dispose())) @@ -19435,6 +19454,11 @@ def to_contain_text( Ensures the `Locator` points to an element that contains the given text. You can use regular expressions for the value as well. + **Details** + + When `expected` parameter is a string, Playwright will normalize whitespaces and line breaks both in the actual + text and in the expected string before matching. When regular expression is used, the actual text is matched as is. + **Usage** ```py @@ -19578,6 +19602,7 @@ def to_have_attribute( name: str, value: typing.Union[str, typing.Pattern[str]], *, + ignore_case: typing.Optional[bool] = None, timeout: typing.Optional[float] = None ) -> None: """LocatorAssertions.to_have_attribute @@ -19606,6 +19631,9 @@ def to_have_attribute( Attribute name. value : Union[Pattern[str], str] Expected attribute value. + ignore_case : Union[bool, None] + Whether to perform case-insensitive match. `ignoreCase` option takes precedence over the corresponding regular + expression flag if specified. timeout : Union[float, None] Time to retry the assertion for in milliseconds. Defaults to `5000`. """ @@ -19614,7 +19642,7 @@ def to_have_attribute( return mapping.from_maybe_impl( self._sync( self._impl_obj.to_have_attribute( - name=name, value=value, timeout=timeout + name=name, value=value, ignore_case=ignore_case, timeout=timeout ) ) ) @@ -20190,6 +20218,11 @@ def to_have_text( Ensures the `Locator` points to an element with the given text. You can use regular expressions for the value as well. + **Details** + + When `expected` parameter is a string, Playwright will normalize whitespaces and line breaks both in the actual + text and in the expected string before matching. When regular expression is used, the actual text is matched as is. + **Usage** ```py diff --git a/setup.py b/setup.py index 4ae0a85e55..2bd953f32d 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.40.0-alpha-oct-18-2023" +driver_version = "1.40.0" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/assets/file-to-upload-2.txt b/tests/assets/file-to-upload-2.txt new file mode 100644 index 0000000000..2e2da21755 --- /dev/null +++ b/tests/assets/file-to-upload-2.txt @@ -0,0 +1 @@ +contents of the file diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 91d16b0c52..49b8603096 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -112,6 +112,15 @@ async def test_assertions_locator_to_have_attribute(page: Page, server: Server) ) +async def test_assertions_locator_to_have_attribute_ignore_case( + page: Page, server: Page +) -> None: + await page.set_content("
Text content
") + locator = page.locator("#NoDe") + await expect(locator).to_have_attribute("id", "node", ignore_case=True) + await expect(locator).not_to_have_attribute("id", "node") + + async def test_assertions_locator_to_have_class(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) await page.set_content("
kek
") diff --git a/tests/async/test_browsertype_connect.py b/tests/async/test_browsertype_connect.py index cafdfaab38..a5c63ae00c 100644 --- a/tests/async/test_browsertype_connect.py +++ b/tests/async/test_browsertype_connect.py @@ -359,3 +359,31 @@ async def test_should_record_trace_with_relative_trace_path( assert Path("trace1.zip").exists() finally: Path("trace1.zip").unlink() + + +async def test_set_input_files_should_preserve_last_modified_timestamp( + browser_type: BrowserType, + launch_server: Callable[[], RemoteServer], + assetdir: Path, +) -> None: + # Launch another server to not affect other tests. + remote = await launch_server() + + browser = await browser_type.connect(remote.ws_endpoint) + context = await browser.new_context() + page = await context.new_page() + + await page.set_content("") + input = page.locator("input") + files = ["file-to-upload.txt", "file-to-upload-2.txt"] + await input.set_input_files([assetdir / file for file in files]) + assert await input.evaluate("input => [...input.files].map(f => f.name)") == files + timestamps = await input.evaluate( + "input => [...input.files].map(f => f.lastModified)" + ) + expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files] + + # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even + # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. + for i in range(len(timestamps)): + assert abs(timestamps[i] - expected_timestamps[i]) < 1000 diff --git a/tests/async/test_evaluate.py b/tests/async/test_evaluate.py index 95c528d336..eb647dc2d0 100644 --- a/tests/async/test_evaluate.py +++ b/tests/async/test_evaluate.py @@ -197,7 +197,7 @@ async def test_evaluate_throw_if_underlying_element_was_disposed(page): await page.evaluate("e => e.textContent", element) except Error as e: error = e - assert "JSHandle is disposed" in error.message + assert "no object with guid" in error.message async def test_evaluate_evaluate_exception(page): diff --git a/tests/async/test_input.py b/tests/async/test_input.py index ead68ecb57..76a8acc4d3 100644 --- a/tests/async/test_input.py +++ b/tests/async/test_input.py @@ -17,6 +17,7 @@ import re import shutil import sys +from pathlib import Path import pytest from flaky import flaky @@ -351,6 +352,26 @@ async def test_should_upload_large_file(page, server, tmp_path): assert match.group("filename") == b"200MB.zip" +async def test_set_input_files_should_preserve_last_modified_timestamp( + page: Page, + assetdir: Path, +) -> None: + await page.set_content("") + input = page.locator("input") + files = ["file-to-upload.txt", "file-to-upload-2.txt"] + await input.set_input_files([assetdir / file for file in files]) + assert await input.evaluate("input => [...input.files].map(f => f.name)") == files + timestamps = await input.evaluate( + "input => [...input.files].map(f => f.lastModified)" + ) + expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files] + + # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even + # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. + for i in range(len(timestamps)): + assert abs(timestamps[i] - expected_timestamps[i]) < 1000 + + @flaky async def test_should_upload_multiple_large_file(page: Page, server, tmp_path): files_count = 10 diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index 3f83eec494..ef66e2af3b 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -105,6 +105,15 @@ def test_assertions_locator_to_have_attribute(page: Page, server: Server) -> Non expect(page.locator("div#foobar")).to_have_attribute("id", "koko", timeout=100) +def test_assertions_locator_to_have_attribute_ignore_case( + page: Page, server: Page +) -> None: + page.set_content("
Text content
") + locator = page.locator("#NoDe") + expect(locator).to_have_attribute("id", "node", ignore_case=True) + expect(locator).not_to_have_attribute("id", "node") + + def test_assertions_locator_to_have_class(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) page.set_content("
kek
") diff --git a/tests/sync/test_browsertype_connect.py b/tests/sync/test_browsertype_connect.py index 26ee44227d..74970c5453 100644 --- a/tests/sync/test_browsertype_connect.py +++ b/tests/sync/test_browsertype_connect.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import time -from typing import Callable +from pathlib import Path +from typing import Any, Callable import pytest @@ -221,3 +223,29 @@ def handle_request(route: Route) -> None: assert response.json() == {"foo": "bar"} remote.kill() + + +def test_set_input_files_should_preserve_last_modified_timestamp( + browser_type: BrowserType, + launch_server: Callable[[], RemoteServer], + assetdir: Path, +) -> None: + # Launch another server to not affect other tests. + remote = launch_server() + + browser = browser_type.connect(remote.ws_endpoint) + context = browser.new_context() + page = context.new_page() + + page.set_content("") + input = page.locator("input") + files: Any = ["file-to-upload.txt", "file-to-upload-2.txt"] + input.set_input_files([assetdir / file for file in files]) + assert input.evaluate("input => [...input.files].map(f => f.name)") == files + timestamps = input.evaluate("input => [...input.files].map(f => f.lastModified)") + expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files] + + # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even + # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. + for i in range(len(timestamps)): + assert abs(timestamps[i] - expected_timestamps[i]) < 1000 diff --git a/tests/sync/test_input.py b/tests/sync/test_input.py index ff28f6a63c..98b4fda55f 100644 --- a/tests/sync/test_input.py +++ b/tests/sync/test_input.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +from pathlib import Path +from typing import Any from playwright.sync_api import Page @@ -24,3 +27,21 @@ def test_expect_file_chooser(page: Page) -> None: fc.set_files( {"name": "test.txt", "mimeType": "text/plain", "buffer": b"Hello World"} ) + + +def test_set_input_files_should_preserve_last_modified_timestamp( + page: Page, + assetdir: Path, +) -> None: + page.set_content("") + input = page.locator("input") + files: Any = ["file-to-upload.txt", "file-to-upload-2.txt"] + input.set_input_files([assetdir / file for file in files]) + assert input.evaluate("input => [...input.files].map(f => f.name)") == files + timestamps = input.evaluate("input => [...input.files].map(f => f.lastModified)") + expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files] + + # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even + # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. + for i in range(len(timestamps)): + assert abs(timestamps[i] - expected_timestamps[i]) < 1000