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("")
+ 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("")
+ 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",