Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->128.0.6613.18<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->129.0.6668.29<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->128.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->130.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
3 changes: 3 additions & 0 deletions playwright/_impl/_api_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ class StorageState(TypedDict, total=False):
class ClientCertificate(TypedDict, total=False):
origin: str
certPath: Optional[Union[str, Path]]
cert: Optional[bytes]
keyPath: Optional[Union[str, Path]]
key: Optional[bytes]
pfxPath: Optional[Union[str, Path]]
pfx: Optional[bytes]
passphrase: Optional[str]


Expand Down
1 change: 0 additions & 1 deletion playwright/_impl/_element_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ async def select_option(
params = locals_to_params(
dict(
timeout=timeout,
noWaitAfter=noWaitAfter,
force=force,
**convert_select_option_values(value, index, label, element)
)
Expand Down
22 changes: 20 additions & 2 deletions playwright/_impl/_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import typing
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, cast
from urllib.parse import parse_qs

import playwright._impl._network as network
from playwright._impl._api_structures import (
Expand Down Expand Up @@ -53,7 +54,7 @@
FormType = Dict[str, Union[bool, float, str]]
DataType = Union[Any, bytes, str]
MultipartType = Dict[str, Union[bytes, bool, float, str, FilePayload]]
ParamsType = Dict[str, Union[bool, float, str]]
ParamsType = Union[Dict[str, Union[bool, float, str]], str]


class APIRequest:
Expand Down Expand Up @@ -404,7 +405,7 @@ async def _inner_fetch(
"fetch",
{
"url": url,
"params": object_to_array(params),
"params": params_to_protocol(params),
"method": method,
"headers": serialized_headers,
"postData": post_data,
Expand All @@ -429,6 +430,23 @@ async def storage_state(
return result


def params_to_protocol(params: Optional[ParamsType]) -> Optional[List[NameValue]]:
if not params:
return None
if isinstance(params, dict):
return object_to_array(params)
if params.startswith("?"):
params = params[1:]
parsed = parse_qs(params)
if not parsed:
return None
out = []
for name, values in parsed.items():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why don't we pass a string over the protocol instead? That would be aligned with our approach to do as much on the server, to avoid aligning the details on the client side. Let's discuss with the team.

for value in values:
out.append(NameValue(name=name, value=value))
return out


def file_payload_to_json(payload: FilePayload) -> ServerFilePayload:
return ServerFilePayload(
name=payload["name"],
Expand Down
1 change: 0 additions & 1 deletion playwright/_impl/_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,6 @@ async def select_option(
dict(
selector=selector,
timeout=timeout,
noWaitAfter=noWaitAfter,
strict=strict,
force=force,
**convert_select_option_values(value, index, label, element),
Expand Down
6 changes: 6 additions & 0 deletions playwright/_impl/_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,20 @@ async def to_client_certificates_protocol(
}
if passphrase := clientCertificate.get("passphrase"):
out_record["passphrase"] = passphrase
if pfx := clientCertificate.get("pfx"):
out_record["pfx"] = base64.b64encode(pfx).decode()
if pfx_path := clientCertificate.get("pfxPath"):
out_record["pfx"] = base64.b64encode(
await async_readfile(pfx_path)
).decode()
if cert := clientCertificate.get("cert"):
out_record["cert"] = base64.b64encode(cert).decode()
if cert_path := clientCertificate.get("certPath"):
out_record["cert"] = base64.b64encode(
await async_readfile(cert_path)
).decode()
if key := clientCertificate.get("key"):
out_record["key"] = base64.b64encode(key).decode()
if key_path := clientCertificate.get("keyPath"):
out_record["key"] = base64.b64encode(
await async_readfile(key_path)
Expand Down
122 changes: 58 additions & 64 deletions playwright/async_api/_generated.py

Large diffs are not rendered by default.

122 changes: 58 additions & 64 deletions playwright/sync_api/_generated.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
InWheel = None
from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand

driver_version = "1.46.0"
driver_version = "1.47.0-beta-1725889926000"


def extractall(zip: zipfile.ZipFile, path: str) -> None:
Expand Down
41 changes: 41 additions & 0 deletions tests/async/test_browsercontext_client_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,47 @@ async def test_should_work_with_new_context(browser: Browser, assetdir: Path) ->
await context.close()


async def test_should_work_with_new_context_passing_as_content(
browser: Browser, assetdir: Path
) -> None:
context = await browser.new_context(
# TODO: Remove this once we can pass a custom CA.
ignore_https_errors=True,
client_certificates=[
{
"origin": "https://127.0.0.1:8000",
"cert": (
assetdir / "client-certificates/client/trusted/cert.pem"
).read_bytes(),
"key": (
assetdir / "client-certificates/client/trusted/key.pem"
).read_bytes(),
}
],
)
page = await context.new_page()
await page.goto("https://localhost:8000")
await expect(page.get_by_test_id("message")).to_have_text(
"Sorry, but you need to provide a client certificate to continue."
)
await page.goto("https://127.0.0.1:8000")
await expect(page.get_by_test_id("message")).to_have_text(
"Hello Alice, your certificate was issued by localhost!"
)

response = await page.context.request.get("https://localhost:8000")
assert (
"Sorry, but you need to provide a client certificate to continue."
in await response.text()
)
response = await page.context.request.get("https://127.0.0.1:8000")
assert (
"Hello Alice, your certificate was issued by localhost!"
in await response.text()
)
await context.close()


async def test_should_work_with_new_persistent_context(
browser_type: BrowserType, assetdir: Path, launch_arguments: Dict
) -> None:
Expand Down
44 changes: 42 additions & 2 deletions tests/async/test_fetch_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,48 @@ async def test_should_support_query_params(
server.EMPTY_PAGE + "?p1=foo", params=expected_params
),
)
assert server_req.args["p1".encode()][0].decode() == "v1"
assert len(server_req.args["p1".encode()]) == 1
assert list(map(lambda x: x.decode(), server_req.args["p1".encode()])) == [
"foo",
"v1",
]
assert server_req.args["парам2".encode()][0].decode() == "знач2"


@pytest.mark.parametrize(
"method", ["fetch", "delete", "get", "head", "patch", "post", "put"]
)
async def test_should_support_params_passed_as_object(
context: BrowserContext, server: Server, method: str
) -> None:
params = {
"param1": "value1",
"парам2": "знач2",
}
[server_req, _] = await asyncio.gather(
server.wait_for_request("/empty.html"),
getattr(context.request, method)(server.EMPTY_PAGE, params=params),
)
assert server_req.args["param1".encode()][0].decode() == "value1"
assert len(server_req.args["param1".encode()]) == 1
assert server_req.args["парам2".encode()][0].decode() == "знач2"


@pytest.mark.parametrize(
"method", ["fetch", "delete", "get", "head", "patch", "post", "put"]
)
async def test_should_support_params_passed_as_strings(
context: BrowserContext, server: Server, method: str
) -> None:
params = "?param1=value1&param1=value2&парам2=знач2"
[server_req, _] = await asyncio.gather(
server.wait_for_request("/empty.html"),
getattr(context.request, method)(server.EMPTY_PAGE, params=params),
)
assert list(map(lambda x: x.decode(), server_req.args["param1".encode()])) == [
"value1",
"value2",
]
assert len(server_req.args["param1".encode()]) == 2
assert server_req.args["парам2".encode()][0].decode() == "знач2"


Expand Down
38 changes: 38 additions & 0 deletions tests/sync/test_browsercontext_client_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,44 @@ def test_should_work_with_new_context(browser: Browser, assetdir: Path) -> None:
context.close()


def test_should_work_with_new_context_passing_as_content(
browser: Browser, assetdir: Path
) -> None:
context = browser.new_context(
# TODO: Remove this once we can pass a custom CA.
ignore_https_errors=True,
client_certificates=[
{
"origin": "https://127.0.0.1:8000",
"cert": (
assetdir / "client-certificates/client/trusted/cert.pem"
).read_bytes(),
"key": (
assetdir / "client-certificates/client/trusted/key.pem"
).read_bytes(),
}
],
)
page = context.new_page()
page.goto("https://localhost:8000")
expect(page.get_by_test_id("message")).to_have_text(
"Sorry, but you need to provide a client certificate to continue."
)
page.goto("https://127.0.0.1:8000")
expect(page.get_by_test_id("message")).to_have_text(
"Hello Alice, your certificate was issued by localhost!"
)

response = page.context.request.get("https://localhost:8000")
assert (
"Sorry, but you need to provide a client certificate to continue."
in response.text()
)
response = page.context.request.get("https://127.0.0.1:8000")
assert "Hello Alice, your certificate was issued by localhost!" in response.text()
context.close()


def test_should_work_with_new_persistent_context(
browser_type: BrowserType, assetdir: Path, launch_arguments: Dict
) -> None:
Expand Down
39 changes: 37 additions & 2 deletions tests/sync/test_fetch_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,43 @@ def test_should_support_query_params(
getattr(context.request, method)(
server.EMPTY_PAGE + "?p1=foo", params=expected_params
)
assert server_req.value.args["p1".encode()][0].decode() == "v1"
assert len(server_req.value.args["p1".encode()]) == 1
assert list(map(lambda x: x.decode(), server_req.value.args["p1".encode()])) == [
"foo",
"v1",
]
assert server_req.value.args["парам2".encode()][0].decode() == "знач2"


@pytest.mark.parametrize(
"method", ["fetch", "delete", "get", "head", "patch", "post", "put"]
)
def test_should_support_params_passed_as_object(
context: BrowserContext, server: Server, method: str
) -> None:
params = {
"param1": "value1",
"парам2": "знач2",
}
with server.expect_request("/empty.html") as server_req:
getattr(context.request, method)(server.EMPTY_PAGE, params=params)
assert server_req.value.args["param1".encode()][0].decode() == "value1"
assert len(server_req.value.args["param1".encode()]) == 1
assert server_req.value.args["парам2".encode()][0].decode() == "знач2"


@pytest.mark.parametrize(
"method", ["fetch", "delete", "get", "head", "patch", "post", "put"]
)
def test_should_support_params_passed_as_strings(
context: BrowserContext, server: Server, method: str
) -> None:
params = "?param1=value1&param1=value2&парам2=знач2"
with server.expect_request("/empty.html") as server_req:
getattr(context.request, method)(server.EMPTY_PAGE, params=params)
assert list(
map(lambda x: x.decode(), server_req.value.args["param1".encode()])
) == ["value1", "value2"]
assert len(server_req.value.args["param1".encode()]) == 2
assert server_req.value.args["парам2".encode()][0].decode() == "знач2"


Expand Down
Loading