diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py index 14202117e..78985a774 100644 --- a/playwright/_impl/_artifact.py +++ b/playwright/_impl/_artifact.py @@ -47,5 +47,10 @@ async def failure(self) -> Optional[str]: async def delete(self) -> None: await self._channel.send("delete") + async def read_info_buffer(self) -> bytes: + stream = cast(Stream, from_channel(await self._channel.send("stream"))) + buffer = await stream.read_all() + return buffer + async def cancel(self) -> None: await self._channel.send("cancel") diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 2c499d5b1..b58782614 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import base64 import json from pathlib import Path from types import SimpleNamespace -from typing import TYPE_CHECKING, Dict, List, Pattern, Union, cast +from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Union, cast from playwright._impl._api_structures import ( Geolocation, @@ -25,6 +24,7 @@ StorageState, ViewportSize, ) +from playwright._impl._artifact import Artifact from playwright._impl._browser_context import BrowserContext from playwright._impl._cdp_session import CDPSession from playwright._impl._connection import ChannelOwner, from_channel @@ -39,6 +39,7 @@ async_readfile, is_safe_close_error, locals_to_params, + make_dirs_for_file, prepare_record_har_options, ) from playwright._impl._network import serialize_headers @@ -61,6 +62,7 @@ def __init__( self._is_connected = True self._is_closed_or_closing = False self._should_close_connection_on_close = False + self._cr_tracing_path: Optional[str] = None self._contexts: List[BrowserContext] = [] self._channel.on("close", lambda _: self._on_close()) @@ -207,12 +209,20 @@ async def start_tracing( if page: params["page"] = page._channel if path: + self._cr_tracing_path = str(path) params["path"] = str(path) await self._channel.send("startTracing", params) async def stop_tracing(self) -> bytes: - encoded_binary = await self._channel.send("stopTracing") - return base64.b64decode(encoded_binary) + artifact = cast(Artifact, from_channel(await self._channel.send("stopTracing"))) + buffer = await artifact.read_info_buffer() + await artifact.delete() + if self._cr_tracing_path: + make_dirs_for_file(self._cr_tracing_path) + with open(self._cr_tracing_path, "wb") as f: + f.write(buffer) + self._cr_tracing_path = None + return buffer async def prepare_browser_context_params(params: Dict) -> None: diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 51a103c76..4d93a9b14 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -29,6 +29,7 @@ from playwright._impl._connection import ( ChannelOwner, Connection, + filter_none, from_channel, from_nullable_channel, ) @@ -187,6 +188,7 @@ async def connect( timeout: float = None, slow_mo: float = None, headers: Dict[str, str] = None, + expose_network: str = None, ) -> Browser: if timeout is None: timeout = 30000 @@ -198,12 +200,15 @@ async def connect( pipe_channel = ( await local_utils._channel.send_return_as_dict( "connect", - { - "wsEndpoint": ws_endpoint, - "headers": headers, - "slowMo": slow_mo, - "timeout": timeout, - }, + filter_none( + { + "wsEndpoint": ws_endpoint, + "headers": headers, + "slowMo": slow_mo, + "timeout": timeout, + "exposeNetwork": expose_network, + } + ), ) )["pipe"] transport = JsonPipeTransport(self._connection._loop, pipe_channel) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 246617d06..409489558 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -241,7 +241,7 @@ def locator( raise Error("Locators must belong to the same frame.") return Locator( self._frame, - f"{self._selector} >> {selector_or_locator._selector}", + f"{self._selector} >> internal:chain={json.dumps(selector_or_locator._selector)}", has_text=has_text, has_not_text=has_not_text, has_not=has_not, diff --git a/playwright/_impl/_stream.py b/playwright/_impl/_stream.py index 762b282c8..d27427589 100644 --- a/playwright/_impl/_stream.py +++ b/playwright/_impl/_stream.py @@ -35,3 +35,12 @@ async def save_as(self, path: Union[str, Path]) -> None: None, lambda: file.write(base64.b64decode(binary)) ) await self._loop.run_in_executor(None, lambda: file.close()) + + async def read_all(self) -> bytes: + binary = b"" + while True: + chunk = await self._channel.send("read", {"size": 1024 * 1024}) + if not chunk: + break + binary += base64.b64decode(chunk) + return binary diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index f73b91c31..1393ca13c 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -4277,6 +4277,9 @@ async def set_content( ) -> None: """Frame.set_content + This method internally calls [document.write()](https://developer.mozilla.org/en-US/docs/Web/API/Document/write), + inheriting all its specific characteristics and behaviors. + Parameters ---------- html : str @@ -9156,6 +9159,9 @@ async def set_content( ) -> None: """Page.set_content + This method internally calls [document.write()](https://developer.mozilla.org/en-US/docs/Web/API/Document/write), + inheriting all its specific characteristics and behaviors. + Parameters ---------- html : str @@ -9854,7 +9860,7 @@ async def route_from_har( """Page.route_from_har If specified the network requests that are made in the page will be served from the HAR file. Read more about - [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + [Replaying from HAR](https://playwright.dev/python/docs/mock#replaying-from-har). Playwright will not serve requests intercepted by Service Worker from the HAR file. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when @@ -13592,7 +13598,7 @@ async def route_from_har( """BrowserContext.route_from_har If specified the network requests that are made in the context will be served from the HAR file. Read more about - [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + [Replaying from HAR](https://playwright.dev/python/docs/mock#replaying-from-har). Playwright will not serve requests intercepted by Service Worker from the HAR file. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when @@ -14088,7 +14094,7 @@ async def new_context( is_mobile : Union[bool, None] Whether the `meta viewport` tag is taken into account and touch events are enabled. isMobile is a part of device, so you don't actually need to set it manually. Defaults to `false` and is not supported in Firefox. Learn more - about [mobile emulation](../emulation.md#isMobile). + about [mobile emulation](../emulation.md#ismobile). has_touch : Union[bool, None] Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). @@ -14302,7 +14308,7 @@ async def new_page( is_mobile : Union[bool, None] Whether the `meta viewport` tag is taken into account and touch events are enabled. isMobile is a part of device, so you don't actually need to set it manually. Defaults to `false` and is not supported in Firefox. Learn more - about [mobile emulation](../emulation.md#isMobile). + about [mobile emulation](../emulation.md#ismobile). has_touch : Union[bool, None] Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). @@ -14847,7 +14853,7 @@ async def launch_persistent_context( is_mobile : Union[bool, None] Whether the `meta viewport` tag is taken into account and touch events are enabled. isMobile is a part of device, so you don't actually need to set it manually. Defaults to `false` and is not supported in Firefox. Learn more - about [mobile emulation](../emulation.md#isMobile). + about [mobile emulation](../emulation.md#ismobile). has_touch : Union[bool, None] Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). @@ -15033,7 +15039,8 @@ async def connect( *, timeout: typing.Optional[float] = None, slow_mo: typing.Optional[float] = None, - headers: typing.Optional[typing.Dict[str, str]] = None + headers: typing.Optional[typing.Dict[str, str]] = None, + expose_network: typing.Optional[str] = None ) -> "Browser": """BrowserType.connect @@ -15052,6 +15059,20 @@ async def connect( on. Defaults to 0. headers : Union[Dict[str, str], None] Additional HTTP headers to be sent with web socket connect request. Optional. + expose_network : Union[str, None] + This option exposes network available on the connecting client to the browser being connected to. Consists of a + list of rules separated by comma. + + Available rules: + 1. Hostname pattern, for example: `example.com`, `*.org:99`, `x.*.y.com`, `*foo.org`. + 1. IP literal, for example: `127.0.0.1`, `0.0.0.0:99`, `[::1]`, `[0:0::1]:99`. + 1. `` that matches local loopback interfaces: `localhost`, `*.localhost`, `127.0.0.1`, `[::1]`. + + Some common examples: + 1. `"*"` to expose all network. + 1. `""` to expose localhost network. + 1. `"*.test.internal-domain,*.staging.internal-domain,"` to expose test/staging deployments and + localhost. Returns ------- @@ -15064,6 +15085,7 @@ async def connect( timeout=timeout, slow_mo=slow_mo, headers=mapping.to_impl(headers), + expose_network=expose_network, ) ) @@ -16833,7 +16855,8 @@ async def blur(self, *, timeout: typing.Optional[float] = None) -> None: async def all(self) -> typing.List["Locator"]: """Locator.all - When locator points to a list of elements, returns array of locators, pointing to respective elements. + When the locator points to a list of elements, this returns an array of locators, pointing to their respective + elements. **NOTE** `locator.all()` does not wait for elements to match the locator, and instead immediately returns whatever is present in the page. When the list of elements changes dynamically, `locator.all()` will @@ -17803,6 +17826,9 @@ async def type( ) -> None: """Locator.type + **NOTE** In most cases, you should use `locator.fill()` instead. You only need to type characters if there + is special keyboard handling on the page. + Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. @@ -19908,16 +19934,16 @@ async def to_have_text( from playwright.sync_api import expect # ✓ Has the right items in the right order - await expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) + expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) # ✖ Wrong order - await expect(page.locator(\"ul > li\")).to_have_text([\"Text 3\", \"Text 2\", \"Text 1\"]) + expect(page.locator(\"ul > li\")).to_have_text([\"Text 3\", \"Text 2\", \"Text 1\"]) # ✖ Last item does not match - await expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text\"]) + expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text\"]) # ✖ Locator points to the outer list element, not to the list items - await expect(page.locator(\"ul\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) + expect(page.locator(\"ul\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) ``` Parameters diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index f1b41ca37..66b989217 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -4353,6 +4353,9 @@ def set_content( ) -> None: """Frame.set_content + This method internally calls [document.write()](https://developer.mozilla.org/en-US/docs/Web/API/Document/write), + inheriting all its specific characteristics and behaviors. + Parameters ---------- html : str @@ -9206,6 +9209,9 @@ def set_content( ) -> None: """Page.set_content + This method internally calls [document.write()](https://developer.mozilla.org/en-US/docs/Web/API/Document/write), + inheriting all its specific characteristics and behaviors. + Parameters ---------- html : str @@ -9920,7 +9926,7 @@ def route_from_har( """Page.route_from_har If specified the network requests that are made in the page will be served from the HAR file. Read more about - [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + [Replaying from HAR](https://playwright.dev/python/docs/mock#replaying-from-har). Playwright will not serve requests intercepted by Service Worker from the HAR file. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when @@ -13652,7 +13658,7 @@ def route_from_har( """BrowserContext.route_from_har If specified the network requests that are made in the context will be served from the HAR file. Read more about - [Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har). + [Replaying from HAR](https://playwright.dev/python/docs/mock#replaying-from-har). Playwright will not serve requests intercepted by Service Worker from the HAR file. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when @@ -14150,7 +14156,7 @@ def new_context( is_mobile : Union[bool, None] Whether the `meta viewport` tag is taken into account and touch events are enabled. isMobile is a part of device, so you don't actually need to set it manually. Defaults to `false` and is not supported in Firefox. Learn more - about [mobile emulation](../emulation.md#isMobile). + about [mobile emulation](../emulation.md#ismobile). has_touch : Union[bool, None] Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). @@ -14366,7 +14372,7 @@ def new_page( is_mobile : Union[bool, None] Whether the `meta viewport` tag is taken into account and touch events are enabled. isMobile is a part of device, so you don't actually need to set it manually. Defaults to `false` and is not supported in Firefox. Learn more - about [mobile emulation](../emulation.md#isMobile). + about [mobile emulation](../emulation.md#ismobile). has_touch : Union[bool, None] Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). @@ -14917,7 +14923,7 @@ def launch_persistent_context( is_mobile : Union[bool, None] Whether the `meta viewport` tag is taken into account and touch events are enabled. isMobile is a part of device, so you don't actually need to set it manually. Defaults to `false` and is not supported in Firefox. Learn more - about [mobile emulation](../emulation.md#isMobile). + about [mobile emulation](../emulation.md#ismobile). has_touch : Union[bool, None] Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). @@ -15107,7 +15113,8 @@ def connect( *, timeout: typing.Optional[float] = None, slow_mo: typing.Optional[float] = None, - headers: typing.Optional[typing.Dict[str, str]] = None + headers: typing.Optional[typing.Dict[str, str]] = None, + expose_network: typing.Optional[str] = None ) -> "Browser": """BrowserType.connect @@ -15126,6 +15133,20 @@ def connect( on. Defaults to 0. headers : Union[Dict[str, str], None] Additional HTTP headers to be sent with web socket connect request. Optional. + expose_network : Union[str, None] + This option exposes network available on the connecting client to the browser being connected to. Consists of a + list of rules separated by comma. + + Available rules: + 1. Hostname pattern, for example: `example.com`, `*.org:99`, `x.*.y.com`, `*foo.org`. + 1. IP literal, for example: `127.0.0.1`, `0.0.0.0:99`, `[::1]`, `[0:0::1]:99`. + 1. `` that matches local loopback interfaces: `localhost`, `*.localhost`, `127.0.0.1`, `[::1]`. + + Some common examples: + 1. `"*"` to expose all network. + 1. `""` to expose localhost network. + 1. `"*.test.internal-domain,*.staging.internal-domain,"` to expose test/staging deployments and + localhost. Returns ------- @@ -15139,6 +15160,7 @@ def connect( timeout=timeout, slow_mo=slow_mo, headers=mapping.to_impl(headers), + expose_network=expose_network, ) ) ) @@ -16933,7 +16955,8 @@ def blur(self, *, timeout: typing.Optional[float] = None) -> None: def all(self) -> typing.List["Locator"]: """Locator.all - When locator points to a list of elements, returns array of locators, pointing to respective elements. + When the locator points to a list of elements, this returns an array of locators, pointing to their respective + elements. **NOTE** `locator.all()` does not wait for elements to match the locator, and instead immediately returns whatever is present in the page. When the list of elements changes dynamically, `locator.all()` will @@ -17931,6 +17954,9 @@ def type( ) -> None: """Locator.type + **NOTE** In most cases, you should use `locator.fill()` instead. You only need to type characters if there + is special keyboard handling on the page. + Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. @@ -20088,16 +20114,16 @@ def to_have_text( from playwright.sync_api import expect # ✓ Has the right items in the right order - await expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) + expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) # ✖ Wrong order - await expect(page.locator(\"ul > li\")).to_have_text([\"Text 3\", \"Text 2\", \"Text 1\"]) + expect(page.locator(\"ul > li\")).to_have_text([\"Text 3\", \"Text 2\", \"Text 1\"]) # ✖ Last item does not match - await expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text\"]) + expect(page.locator(\"ul > li\")).to_have_text([\"Text 1\", \"Text 2\", \"Text\"]) # ✖ Locator points to the outer list element, not to the list items - await expect(page.locator(\"ul\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) + expect(page.locator(\"ul\")).to_have_text([\"Text 1\", \"Text 2\", \"Text 3\"]) ``` Parameters diff --git a/setup.py b/setup.py index 79ab19e30..462453adb 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.36.0" +driver_version = "1.37.0-alpha-aug-8-2023" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py index 2de3a244c..0630fadad 100644 --- a/tests/async/test_locators.py +++ b/tests/async/test_locators.py @@ -846,6 +846,35 @@ async def test_locator_should_support_locator_or(page: Page, server: Server) -> ) +async def test_locator_should_support_locator_locator_with_and_or(page: Page) -> None: + await page.set_content( + """ +
one two
+ four + + """ + ) + + await expect(page.locator("div").locator(page.locator("button"))).to_have_text( + ["three"] + ) + await expect( + page.locator("div").locator(page.locator("button").or_(page.locator("span"))) + ).to_have_text(["two", "three"]) + await expect(page.locator("button").or_(page.locator("span"))).to_have_text( + ["two", "three", "four", "five"] + ) + + await expect( + page.locator("div").locator( + page.locator("button").and_(page.get_by_role("button")) + ) + ).to_have_text(["three"]) + await expect(page.locator("button").and_(page.get_by_role("button"))).to_have_text( + ["three", "five"] + ) + + async def test_locator_highlight_should_work(page: Page, server: Server) -> None: await page.goto(server.PREFIX + "/grid.html") await page.locator(".box").nth(3).highlight() diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py index bf41f6938..89fec6700 100644 --- a/tests/async/test_navigation.py +++ b/tests/async/test_navigation.py @@ -906,11 +906,11 @@ async def test_frame_goto_should_reject_when_frame_detaches(page, server, browse with pytest.raises(Error) as exc_info: await navigation_task if browser_name == "chromium": - assert ("frame was detached" in exc_info.value.message) or ( - "net::ERR_ABORTED" in exc_info.value.message + assert "net::ERR_FAILED" in exc_info.value.message or ( + "frame was detached" in exc_info.value.message.lower() ) else: - assert "frame was detached" in exc_info.value.message + assert "frame was detached" in exc_info.value.message.lower() async def test_frame_goto_should_continue_after_client_redirect(page, server):