diff --git a/README.md b/README.md index 611573b16..528570e8f 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 123.0.6312.4 | ✅ | ✅ | ✅ | +| Chromium 124.0.6367.8 | ✅ | ✅ | ✅ | | WebKit 17.4 | ✅ | ✅ | ✅ | -| Firefox 123.0 | ✅ | ✅ | ✅ | +| Firefox 124.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index c52a134c8..c540ce4c0 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -70,6 +70,7 @@ ) from playwright._impl._network import Request, Response, Route, serialize_headers from playwright._impl._page import BindingCall, Page, Worker +from playwright._impl._str_utils import escape_regex_flags from playwright._impl._tracing import Tracing from playwright._impl._waiter import Waiter from playwright._impl._web_error import WebError @@ -302,8 +303,34 @@ async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None: await self._channel.send("addCookies", dict(cookies=cookies)) - async def clear_cookies(self) -> None: - await self._channel.send("clearCookies") + async def clear_cookies( + self, + name: Union[str, Pattern[str]] = None, + domain: Union[str, Pattern[str]] = None, + path: Union[str, Pattern[str]] = None, + ) -> None: + await self._channel.send( + "clearCookies", + { + "name": name if isinstance(name, str) else None, + "nameRegexSource": name.pattern if isinstance(name, Pattern) else None, + "nameRegexFlags": escape_regex_flags(name) + if isinstance(name, Pattern) + else None, + "domain": domain if isinstance(domain, str) else None, + "domainRegexSource": domain.pattern + if isinstance(domain, Pattern) + else None, + "domainRegexFlags": escape_regex_flags(domain) + if isinstance(domain, Pattern) + else None, + "path": path if isinstance(path, str) else None, + "pathRegexSource": path.pattern if isinstance(path, Pattern) else None, + "pathRegexFlags": escape_regex_flags(path) + if isinstance(path, Pattern) + else None, + }, + ) async def grant_permissions( self, permissions: Sequence[str], origin: str = None diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index 4db0e2635..415d79a76 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -20,6 +20,7 @@ from urllib.parse import ParseResult, urlparse, urlunparse from playwright._impl._connection import Channel, ChannelOwner, from_channel +from playwright._impl._errors import is_target_closed_error from playwright._impl._map import Map if TYPE_CHECKING: # pragma: no cover @@ -102,7 +103,11 @@ def as_element(self) -> Optional["ElementHandle"]: return None async def dispose(self) -> None: - await self._channel.send("dispose") + try: + await self._channel.send("dispose") + except Exception as e: + if not is_target_closed_error(e): + raise e async def json_value(self) -> Any: return parse_result(await self._channel.send("jsonValue")) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 5d220f13b..c5e92d874 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -325,6 +325,10 @@ def last(self) -> "Locator": def nth(self, index: int) -> "Locator": return Locator(self._frame, f"{self._selector} >> nth={index}") + @property + def content_frame(self) -> "FrameLocator": + return FrameLocator(self._frame, self._selector) + def filter( self, hasText: Union[str, Pattern[str]] = None, @@ -817,6 +821,10 @@ def first(self) -> "FrameLocator": def last(self) -> "FrameLocator": return FrameLocator(self._frame, f"{self._frame_selector} >> nth=-1") + @property + def owner(self) -> "Locator": + return Locator(self._frame, self._frame_selector) + def nth(self, index: int) -> "FrameLocator": return FrameLocator(self._frame, f"{self._frame_selector} >> nth={index}") diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 9d1337b01..1fe436c80 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -169,7 +169,7 @@ def post_data_json(self) -> Optional[Any]: if not post_data: return None content_type = self.headers["content-type"] - if content_type == "application/x-www-form-urlencoded": + if "application/x-www-form-urlencoded" in content_type: return dict(parse.parse_qsl(post_data)) try: return json.loads(post_data) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 3e266ccf0..244a891e3 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -5862,6 +5862,32 @@ def last(self) -> "FrameLocator": """ return mapping.from_impl(self._impl_obj.last) + @property + def owner(self) -> "Locator": + """FrameLocator.owner + + Returns a `Locator` object pointing to the same `iframe` as this frame locator. + + Useful when you have a `FrameLocator` object obtained somewhere, and later on would like to interact with the + `iframe` element. + + For a reverse operation, use `locator.content_frame()`. + + **Usage** + + ```py + frame_locator = page.frame_locator(\"iframe[name=\\\"embedded\\\"]\") + # ... + locator = frame_locator.owner + await expect(locator).to_be_visible() + ``` + + Returns + ------- + Locator + """ + return mapping.from_impl(self._impl_obj.owner) + def locator( self, selector_or_locator: typing.Union["Locator", str], @@ -11669,9 +11695,11 @@ async def add_locator_handler( ) -> None: """Page.add_locator_handler - When testing a web page, sometimes unexpected overlays like a coookie consent dialog appear and block actions you - want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, - making them tricky to handle in automated tests. + **NOTE** This method is experimental and its behavior may change in the upcoming releases. + + When testing a web page, sometimes unexpected overlays like a \"Sign up\" dialog appear and block actions you want to + automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making + them tricky to handle in automated tests. This method lets you set up a special function, called a handler, that activates when it detects that overlay is visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there. @@ -11681,7 +11709,9 @@ async def add_locator_handler( a part of your normal test flow, instead of using `page.add_locator_handler()`. - Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](https://playwright.dev/python/docs/actionability), or before performing an auto-waiting assertion check. When overlay - is visible, Playwright calls the handler first, and then proceeds with the action/assertion. + is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the + handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't + perform any actions, the handler will not be triggered. - The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts. - You can register multiple handlers. However, only a single handler will be running at a time. Make sure the @@ -11699,13 +11729,13 @@ async def add_locator_handler( **Usage** - An example that closes a cookie consent dialog when it appears: + An example that closes a \"Sign up to the newsletter\" dialog when it appears: ```py # Setup the handler. def handler(): - page.get_by_role(\"button\", name=\"Reject all cookies\").click() - page.add_locator_handler(page.get_by_role(\"button\", name=\"Accept all cookies\"), handler) + page.get_by_role(\"button\", name=\"No thanks\").click() + page.add_locator_handler(page.get_by_text(\"Sign up to the newsletter\"), handler) # Write the test as usual. page.goto(\"https://example.com\") @@ -12319,13 +12349,40 @@ async def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: await self._impl_obj.add_cookies(cookies=mapping.to_impl(cookies)) ) - async def clear_cookies(self) -> None: + async def clear_cookies( + self, + *, + name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + domain: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + path: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None + ) -> None: """BrowserContext.clear_cookies - Clears context cookies. + Removes cookies from context. Accepts optional filter. + + **Usage** + + ```py + await context.clear_cookies() + await context.clear_cookies(name=\"session-id\") + await context.clear_cookies(domain=\"my-origin.com\") + await context.clear_cookies(path=\"/api/v1\") + await context.clear_cookies(name=\"session-id\", domain=\"my-origin.com\") + ``` + + Parameters + ---------- + name : Union[Pattern[str], str, None] + Only removes cookies with the given name. + domain : Union[Pattern[str], str, None] + Only removes cookies with the given domain. + path : Union[Pattern[str], str, None] + Only removes cookies with the given path. """ - return mapping.from_maybe_impl(await self._impl_obj.clear_cookies()) + return mapping.from_maybe_impl( + await self._impl_obj.clear_cookies(name=name, domain=domain, path=path) + ) async def grant_permissions( self, permissions: typing.Sequence[str], *, origin: typing.Optional[str] = None @@ -13798,6 +13855,7 @@ async def launch( devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + Deprecated: Use [debugging tools](../debug.md) instead. proxy : Union[{server: str, bypass: Union[str, None], username: Union[str, None], password: Union[str, None]}, None] Network proxy settings. downloads_path : Union[pathlib.Path, str, None] @@ -13955,6 +14013,7 @@ async def launch_persistent_context( devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + Deprecated: Use [debugging tools](../debug.md) instead. proxy : Union[{server: str, bypass: Union[str, None], username: Union[str, None], password: Union[str, None]}, None] Network proxy settings. downloads_path : Union[pathlib.Path, str, None] @@ -14538,6 +14597,32 @@ def last(self) -> "Locator": """ return mapping.from_impl(self._impl_obj.last) + @property + def content_frame(self) -> "FrameLocator": + """Locator.content_frame + + Returns a `FrameLocator` object pointing to the same `iframe` as this locator. + + Useful when you have a `Locator` object obtained somewhere, and later on would like to interact with the content + inside the frame. + + For a reverse operation, use `frame_locator.owner()`. + + **Usage** + + ```py + locator = page.locator(\"iframe[name=\\\"embedded\\\"]\") + # ... + frame_locator = locator.content_frame + await frame_locator.get_by_role(\"button\").click() + ``` + + Returns + ------- + FrameLocator + """ + return mapping.from_impl(self._impl_obj.content_frame) + async def bounding_box( self, *, timeout: typing.Optional[float] = None ) -> typing.Optional[FloatRect]: diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index d6ecf78ad..6c1fe5fbb 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -5976,6 +5976,32 @@ def last(self) -> "FrameLocator": """ return mapping.from_impl(self._impl_obj.last) + @property + def owner(self) -> "Locator": + """FrameLocator.owner + + Returns a `Locator` object pointing to the same `iframe` as this frame locator. + + Useful when you have a `FrameLocator` object obtained somewhere, and later on would like to interact with the + `iframe` element. + + For a reverse operation, use `locator.content_frame()`. + + **Usage** + + ```py + frame_locator = page.frame_locator(\"iframe[name=\\\"embedded\\\"]\") + # ... + locator = frame_locator.owner + expect(locator).to_be_visible() + ``` + + Returns + ------- + Locator + """ + return mapping.from_impl(self._impl_obj.owner) + def locator( self, selector_or_locator: typing.Union["Locator", str], @@ -11752,9 +11778,11 @@ def set_checked( def add_locator_handler(self, locator: "Locator", handler: typing.Callable) -> None: """Page.add_locator_handler - When testing a web page, sometimes unexpected overlays like a coookie consent dialog appear and block actions you - want to automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, - making them tricky to handle in automated tests. + **NOTE** This method is experimental and its behavior may change in the upcoming releases. + + When testing a web page, sometimes unexpected overlays like a \"Sign up\" dialog appear and block actions you want to + automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making + them tricky to handle in automated tests. This method lets you set up a special function, called a handler, that activates when it detects that overlay is visible. The handler's job is to remove the overlay, allowing your test to continue as if the overlay wasn't there. @@ -11764,7 +11792,9 @@ def add_locator_handler(self, locator: "Locator", handler: typing.Callable) -> N a part of your normal test flow, instead of using `page.add_locator_handler()`. - Playwright checks for the overlay every time before executing or retrying an action that requires an [actionability check](https://playwright.dev/python/docs/actionability), or before performing an auto-waiting assertion check. When overlay - is visible, Playwright calls the handler first, and then proceeds with the action/assertion. + is visible, Playwright calls the handler first, and then proceeds with the action/assertion. Note that the + handler is only called when you perform an action/assertion - if the overlay becomes visible but you don't + perform any actions, the handler will not be triggered. - The execution time of the handler counts towards the timeout of the action/assertion that executed the handler. If your handler takes too long, it might cause timeouts. - You can register multiple handlers. However, only a single handler will be running at a time. Make sure the @@ -11782,13 +11812,13 @@ def add_locator_handler(self, locator: "Locator", handler: typing.Callable) -> N **Usage** - An example that closes a cookie consent dialog when it appears: + An example that closes a \"Sign up to the newsletter\" dialog when it appears: ```py # Setup the handler. def handler(): - await page.get_by_role(\"button\", name=\"Reject all cookies\").click() - await page.add_locator_handler(page.get_by_role(\"button\", name=\"Accept all cookies\"), handler) + await page.get_by_role(\"button\", name=\"No thanks\").click() + await page.add_locator_handler(page.get_by_text(\"Sign up to the newsletter\"), handler) # Write the test as usual. await page.goto(\"https://example.com\") @@ -12338,13 +12368,42 @@ def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: self._sync(self._impl_obj.add_cookies(cookies=mapping.to_impl(cookies))) ) - def clear_cookies(self) -> None: + def clear_cookies( + self, + *, + name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + domain: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + path: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None + ) -> None: """BrowserContext.clear_cookies - Clears context cookies. + Removes cookies from context. Accepts optional filter. + + **Usage** + + ```py + context.clear_cookies() + context.clear_cookies(name=\"session-id\") + context.clear_cookies(domain=\"my-origin.com\") + context.clear_cookies(path=\"/api/v1\") + context.clear_cookies(name=\"session-id\", domain=\"my-origin.com\") + ``` + + Parameters + ---------- + name : Union[Pattern[str], str, None] + Only removes cookies with the given name. + domain : Union[Pattern[str], str, None] + Only removes cookies with the given domain. + path : Union[Pattern[str], str, None] + Only removes cookies with the given path. """ - return mapping.from_maybe_impl(self._sync(self._impl_obj.clear_cookies())) + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.clear_cookies(name=name, domain=domain, path=path) + ) + ) def grant_permissions( self, permissions: typing.Sequence[str], *, origin: typing.Optional[str] = None @@ -13828,6 +13887,7 @@ def launch( devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + Deprecated: Use [debugging tools](../debug.md) instead. proxy : Union[{server: str, bypass: Union[str, None], username: Union[str, None], password: Union[str, None]}, None] Network proxy settings. downloads_path : Union[pathlib.Path, str, None] @@ -13987,6 +14047,7 @@ def launch_persistent_context( devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. + Deprecated: Use [debugging tools](../debug.md) instead. proxy : Union[{server: str, bypass: Union[str, None], username: Union[str, None], password: Union[str, None]}, None] Network proxy settings. downloads_path : Union[pathlib.Path, str, None] @@ -14575,6 +14636,32 @@ def last(self) -> "Locator": """ return mapping.from_impl(self._impl_obj.last) + @property + def content_frame(self) -> "FrameLocator": + """Locator.content_frame + + Returns a `FrameLocator` object pointing to the same `iframe` as this locator. + + Useful when you have a `Locator` object obtained somewhere, and later on would like to interact with the content + inside the frame. + + For a reverse operation, use `frame_locator.owner()`. + + **Usage** + + ```py + locator = page.locator(\"iframe[name=\\\"embedded\\\"]\") + # ... + frame_locator = locator.content_frame + frame_locator.get_by_role(\"button\").click() + ``` + + Returns + ------- + FrameLocator + """ + return mapping.from_impl(self._impl_obj.content_frame) + def bounding_box( self, *, timeout: typing.Optional[float] = None ) -> typing.Optional[FloatRect]: diff --git a/setup.py b/setup.py index db21211fb..ae859c6ad 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.42.1" +driver_version = "1.43.0-beta-1711484700000" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_browsercontext_clearcookies.py b/tests/async/test_browsercontext_clearcookies.py index 6b0f03283..336c99718 100644 --- a/tests/async/test_browsercontext_clearcookies.py +++ b/tests/async/test_browsercontext_clearcookies.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re +from urllib.parse import urlparse + from playwright.async_api import Browser, BrowserContext, Page from tests.server import Server @@ -52,3 +55,144 @@ async def test_should_isolate_cookies_when_clearing( assert len(await context.cookies()) == 0 assert len(await another_context.cookies()) == 0 await another_context.close() + + +async def test_should_remove_cookies_by_name( + context: BrowserContext, page: Page, server: Server +) -> None: + await context.add_cookies( + [ + { + "name": "cookie1", + "value": "1", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + { + "name": "cookie2", + "value": "2", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + ] + ) + await page.goto(server.PREFIX) + assert await page.evaluate("document.cookie") == "cookie1=1; cookie2=2" + await context.clear_cookies(name="cookie1") + assert await page.evaluate("document.cookie") == "cookie2=2" + + +async def test_should_remove_cookies_by_name_regex( + context: BrowserContext, page: Page, server: Server +) -> None: + await context.add_cookies( + [ + { + "name": "cookie1", + "value": "1", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + { + "name": "cookie2", + "value": "2", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + ] + ) + await page.goto(server.PREFIX) + assert await page.evaluate("document.cookie") == "cookie1=1; cookie2=2" + await context.clear_cookies(name=re.compile("coo.*1")) + assert await page.evaluate("document.cookie") == "cookie2=2" + + +async def test_should_remove_cookies_by_domain( + context: BrowserContext, page: Page, server: Server +) -> None: + await context.add_cookies( + [ + { + "name": "cookie1", + "value": "1", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + { + "name": "cookie2", + "value": "2", + "domain": urlparse(server.CROSS_PROCESS_PREFIX).hostname, + "path": "/", + }, + ] + ) + await page.goto(server.PREFIX) + assert await page.evaluate("document.cookie") == "cookie1=1" + await page.goto(server.CROSS_PROCESS_PREFIX) + assert await page.evaluate("document.cookie") == "cookie2=2" + await context.clear_cookies(domain=urlparse(server.CROSS_PROCESS_PREFIX).hostname) + assert await page.evaluate("document.cookie") == "" + await page.goto(server.PREFIX) + assert await page.evaluate("document.cookie") == "cookie1=1" + + +async def test_should_remove_cookies_by_path( + context: BrowserContext, page: Page, server: Server +) -> None: + await context.add_cookies( + [ + { + "name": "cookie1", + "value": "1", + "domain": urlparse(server.PREFIX).hostname, + "path": "/api/v1", + }, + { + "name": "cookie2", + "value": "2", + "domain": urlparse(server.PREFIX).hostname, + "path": "/api/v2", + }, + { + "name": "cookie3", + "value": "3", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + ] + ) + await page.goto(server.PREFIX + "/api/v1") + assert await page.evaluate("document.cookie") == "cookie1=1; cookie3=3" + await context.clear_cookies(path="/api/v1") + assert await page.evaluate("document.cookie") == "cookie3=3" + await page.goto(server.PREFIX + "/api/v2") + assert await page.evaluate("document.cookie") == "cookie2=2; cookie3=3" + await page.goto(server.PREFIX + "/") + assert await page.evaluate("document.cookie") == "cookie3=3" + + +async def test_should_remove_cookies_by_name_and_domain( + context: BrowserContext, page: Page, server: Server +) -> None: + await context.add_cookies( + [ + { + "name": "cookie1", + "value": "1", + "domain": urlparse(server.PREFIX).hostname, + "path": "/", + }, + { + "name": "cookie1", + "value": "1", + "domain": urlparse(server.CROSS_PROCESS_PREFIX).hostname, + "path": "/", + }, + ] + ) + await page.goto(server.PREFIX) + assert await page.evaluate("document.cookie") == "cookie1=1" + await context.clear_cookies(name="cookie1", domain=urlparse(server.PREFIX).hostname) + assert await page.evaluate("document.cookie") == "" + await page.goto(server.CROSS_PROCESS_PREFIX) + assert await page.evaluate("document.cookie") == "cookie1=1" diff --git a/tests/async/test_browsercontext_storage_state.py b/tests/async/test_browsercontext_storage_state.py index 9aca1865b..f11aa8281 100644 --- a/tests/async/test_browsercontext_storage_state.py +++ b/tests/async/test_browsercontext_storage_state.py @@ -34,13 +34,13 @@ async def test_should_capture_local_storage(context: BrowserContext) -> None: origins = state["origins"] assert len(origins) == 2 assert origins[0] == { - "origin": "https://www.example.com", - "localStorage": [{"name": "name1", "value": "value1"}], - } - assert origins[1] == { "origin": "https://www.domain.com", "localStorage": [{"name": "name2", "value": "value2"}], } + assert origins[1] == { + "origin": "https://www.example.com", + "localStorage": [{"name": "name1", "value": "value1"}], + } async def test_should_set_local_storage(browser: Browser) -> None: diff --git a/tests/async/test_element_handle.py b/tests/async/test_element_handle.py index 379e78c18..847b068be 100644 --- a/tests/async/test_element_handle.py +++ b/tests/async/test_element_handle.py @@ -784,3 +784,11 @@ async def test_set_checked(page: Page) -> None: assert await page.evaluate("checkbox.checked") await input.set_checked(False) assert await page.evaluate("checkbox.checked") is False + + +async def test_should_allow_disposing_twice(page: Page) -> None: + await page.set_content("
39
") + element = await page.query_selector("section") + assert element + await element.dispose() + await element.dispose() diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py index e725f13b7..b12a25a56 100644 --- a/tests/async/test_locators.py +++ b/tests/async/test_locators.py @@ -580,7 +580,8 @@ async def route_iframe(page: Page) -> None: await page.route( "**/empty.html", lambda route: route.fulfill( - body='', content_type="text/html" + body='', + content_type="text/html", ), ) await page.route( @@ -639,6 +640,26 @@ async def test_locators_frame_should_work_with_locator_frame_locator( await button.click() +async def test_locator_content_frame_should_work(page: Page, server: Server) -> None: + await route_iframe(page) + await page.goto(server.EMPTY_PAGE) + locator = page.locator("iframe") + frame_locator = locator.content_frame + button = frame_locator.locator("button") + assert await button.inner_text() == "Hello iframe" + await expect(button).to_have_text("Hello iframe") + await button.click() + + +async def test_frame_locator_owner_should_work(page: Page, server: Server) -> None: + await route_iframe(page) + await page.goto(server.EMPTY_PAGE) + frame_locator = page.frame_locator("iframe") + locator = frame_locator.owner + await expect(locator).to_be_visible() + assert await locator.get_attribute("name") == "frame1" + + async def route_ambiguous(page: Page) -> None: await page.route( "**/empty.html", diff --git a/tests/async/test_page_network_request.py b/tests/async/test_page_network_request.py index 375342ae8..779875eda 100644 --- a/tests/async/test_page_network_request.py +++ b/tests/async/test_page_network_request.py @@ -42,3 +42,22 @@ async def test_should_not_allow_to_access_frame_on_popup_main_request( await response.finished() await popup_promise await clicked + + +async def test_should_parse_the_data_if_content_type_is_application_x_www_form_urlencoded_charset_UTF_8( + page: Page, server: Server +) -> None: + await page.goto(server.EMPTY_PAGE) + async with page.expect_event("request") as request_info: + await page.evaluate( + """() => fetch('./post', { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: 'foo=bar&baz=123' + })""" + ) + request = await request_info.value + assert request + assert request.post_data_json == {"foo": "bar", "baz": "123"} diff --git a/tests/sync/test_browsercontext_storage_state.py b/tests/sync/test_browsercontext_storage_state.py index fc901a5cf..c785b1479 100644 --- a/tests/sync/test_browsercontext_storage_state.py +++ b/tests/sync/test_browsercontext_storage_state.py @@ -31,13 +31,13 @@ def test_should_capture_local_storage(context: BrowserContext) -> None: assert origins assert len(origins) == 2 assert origins[0] == { - "origin": "https://www.example.com", - "localStorage": [{"name": "name1", "value": "value1"}], - } - assert origins[1] == { "origin": "https://www.domain.com", "localStorage": [{"name": "name2", "value": "value2"}], } + assert origins[1] == { + "origin": "https://www.example.com", + "localStorage": [{"name": "name1", "value": "value1"}], + } def test_should_set_local_storage(browser: Browser) -> None: diff --git a/tests/sync/test_element_handle.py b/tests/sync/test_element_handle.py index 89f6ae2b1..c2faa4a6e 100644 --- a/tests/sync/test_element_handle.py +++ b/tests/sync/test_element_handle.py @@ -661,3 +661,11 @@ def test_set_checked(page: Page) -> None: assert page.evaluate("checkbox.checked") input.set_checked(False) assert page.evaluate("checkbox.checked") is False + + +def test_should_allow_disposing_twice(page: Page) -> None: + page.set_content("
39
") + element = page.query_selector("section") + assert element + element.dispose() + element.dispose() diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py index 4c607d15f..07509e10e 100644 --- a/tests/sync/test_locators.py +++ b/tests/sync/test_locators.py @@ -530,7 +530,8 @@ def route_iframe(page: Page) -> None: page.route( "**/empty.html", lambda route: route.fulfill( - body='', content_type="text/html" + body='', + content_type="text/html", ), ) page.route( @@ -591,6 +592,26 @@ def test_locators_frame_should_work_with_locator_frame_locator( button.click() +def test_locator_content_frame_should_work(page: Page, server: Server) -> None: + route_iframe(page) + page.goto(server.EMPTY_PAGE) + locator = page.locator("iframe") + frame_locator = locator.content_frame + button = frame_locator.locator("button") + assert button.inner_text() == "Hello iframe" + expect(button).to_have_text("Hello iframe") + button.click() + + +def test_frame_locator_owner_should_work(page: Page, server: Server) -> None: + route_iframe(page) + page.goto(server.EMPTY_PAGE) + frame_locator = page.frame_locator("iframe") + locator = frame_locator.owner + expect(locator).to_be_visible() + assert locator.get_attribute("name") == "frame1" + + def route_ambiguous(page: Page) -> None: page.route( "**/empty.html",