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
2 changes: 1 addition & 1 deletion DRIVER_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b335271cdc2b57cac2be8ad37c5dd283ac6b8938
6e2c003f16e01822c3ea45bc894c2542a6e98189
2 changes: 1 addition & 1 deletion NODE_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24.22.0
24.22.1
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->156.0.8078.12<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->27.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->157.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->158.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
7 changes: 6 additions & 1 deletion playwright/_impl/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -2207,7 +2207,12 @@
},
"webStorageClear": {"kind": {"type": "enum", "literals": ["local", "session"]}},
},
"Root": {"initialize": {"sdkLanguage": "SDKLanguage"}},
"Root": {
"initialize": {
"sdkLanguage": "SDKLanguage",
"supportsLoneSurrogates": "boolean?",
}
},
"Playwright": {
"newRequest": {
"baseURL": "string?",
Expand Down
5 changes: 4 additions & 1 deletion playwright/_impl/browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import base64
import json
import os
import re
from pathlib import Path
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -324,7 +325,9 @@ async def _initialize_har_from_options(
if not record_har_path:
return
default_content: Literal["attach", "embed"] = (
"attach" if str(record_har_path).endswith(".zip") else "embed"
"attach"
if re.search(r"\.zip$", str(record_har_path), re.IGNORECASE)
else "embed"
)
await self.tracing._record_into_har(
har=record_har_path,
Expand Down
3 changes: 3 additions & 0 deletions playwright/_impl/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ async def initialize(self) -> "Playwright":
await self._channel.initialize(
# Python: the SDK language of this client.
sdkLanguage="python",
# json.dumps() escapes a lone surrogate as \udxxx, and json.loads()
# reads it back, as the JS client does.
supportsLoneSurrogates=True,
timeout=NO_TIMEOUT,
)
)["playwright"]
Expand Down
1 change: 1 addition & 0 deletions playwright/_impl/protocol/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4181,6 +4181,7 @@
scheme["RootInitializeParams"] = t_object(
{
"sdkLanguage": t_type("SDKLanguage"),
"supportsLoneSurrogates": t_optional(t_boolean),
}
)
scheme["RootInitializeResult"] = t_object(
Expand Down
6 changes: 5 additions & 1 deletion playwright/_impl/protocol_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4952,11 +4952,15 @@ class RootChannel(Channel):
def initialize(
self,
sdkLanguage: channels.SDKLanguage,
supportsLoneSurrogates: Optional[bool],
timeout: float = 0,
) -> Awaitable[channels.RootInitializeResult]:
return self._call(
"initialize",
{"sdkLanguage": sdkLanguage},
{
"sdkLanguage": sdkLanguage,
"supportsLoneSurrogates": supportsLoneSurrogates,
},
timeout,
lambda result: {"playwright": result["playwright"]},
is_internal=True,
Expand Down
6 changes: 3 additions & 3 deletions playwright/_impl/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ async def start_har(
# Not ported: the check of the resourcesDir option against a .zip har file;
# the option is JavaScript only.
default_content: Literal["attach", "embed"] = (
"attach" if str(path).endswith(".zip") else "embed"
"attach" if re.search(r"\.zip$", str(path), re.IGNORECASE) else "embed"
)
self._har_id = await self._record_into_har(
har=path,
Expand Down Expand Up @@ -222,7 +222,7 @@ async def _record_into_har(
# Not ported: the resourcesDir option; it is JavaScript only.
# Python: the path can be a pathlib.Path.
har = str(har)
is_zip = har.endswith(".zip")
is_zip = bool(re.search(r"\.zip$", har, re.IGNORECASE))
options: Dict[str, Any] = {
"content": update_content if update_content is not None else "attach",
"urlGlob": url if isinstance(url, str) else None,
Expand Down Expand Up @@ -250,7 +250,7 @@ async def _export_har(self, har_id: str) -> None:
return
del self._har_recorders[har_id]
is_local = not self._connection.is_remote()
is_zip = har_params["path"].endswith(".zip")
is_zip = bool(re.search(r"\.zip$", har_params["path"], re.IGNORECASE))

if is_local:
entries = (
Expand Down
8 changes: 7 additions & 1 deletion tests/async/library/test_browsercontext_har.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,14 +292,20 @@ async def test_HA_085_should_round_trip_har_zip(
server: Server,
tmp_path: Path,
) -> None:
har_path = tmp_path / "har.zip"
har_path = tmp_path / "har.ZIP"
context1 = await context_factory(
record_har_mode="minimal", record_har_path=har_path
)
page1 = await context1.new_page()
await page1.goto(server.PREFIX + "/one-style.html")
await context1.close()

har_dir = tmp_path / "har"
with zipfile.ZipFile(har_path) as zip_file:
zip_file.extractall(har_dir)
log = json.loads((har_dir / "har.har").read_text(encoding="utf-8"))["log"]
assert isinstance(log["entries"][0]["response"]["content"]["_file"], str)

context2 = await context_factory()
await context2.route_from_har(har_path, not_found="abort")
page2 = await context2.new_page()
Expand Down
25 changes: 20 additions & 5 deletions tests/async/library/test_browsercontext_locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,33 @@ async def test_LC_010_should_propagate_locale_to_workers(
context = await browser.new_context(locale="ru-RU")
page = await context.new_page()
await page.goto(server.EMPTY_PAGE)
async with page.expect_console_message(
lambda e: e.text.startswith("locale:")
) as msg_info:
await page.evaluate(
"""() => new Worker(URL.createObjectURL(new Blob(['console.log("locale:" + Intl.NumberFormat().resolvedOptions().locale)'], { type: 'application/javascript' })))"""
async with (
page.expect_console_message(lambda e: e.text.startswith("locale:")) as msg_info,
page.expect_console_message(
lambda e: e.text.startswith("languages:")
) as languages_info,
):
request, _ = await asyncio.gather(
server.wait_for_request("/title.html"),
page.evaluate(
"""url => new Worker(URL.createObjectURL(new Blob([`
console.log("locale:" + Intl.NumberFormat().resolvedOptions().locale);
console.log("languages:" + navigator.languages);
fetch(${JSON.stringify(url)});
`], { type: 'application/javascript' })))""",
server.PREFIX + "/title.html",
),
)
msg = await msg_info.value
languages = await languages_info.value
if browser_name == "webkit":
# Webkit on Ubuntu is "ru-RU", and on other platforms is "ru"
assert "locale:ru" in msg.text
assert "languages:ru" in languages.text
else:
assert msg.text == "locale:ru-RU"
assert languages.text == "languages:ru-RU"
assert "ru-RU" in (request.getHeader("accept-language") or "")
await context.close()


Expand Down
6 changes: 0 additions & 6 deletions tests/async/library/test_browsercontext_storage_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,12 +433,6 @@ async def test_ST_010_should_handle_malformed_file(
) in str(exc_info.value)


@pytest.mark.xfail(
reason="driver: it replaces lone surrogates with U+FFFD in the messages to the "
"clients other than JavaScript (toWellFormed() in cli/driver.ts), "
"https://github.com/microsoft/playwright/issues/43542",
strict=True,
)
async def test_ST_011_should_serialize_storage_state_with_lone_surrogates(
page: Page, context: BrowserContext, server: Server
) -> None:
Expand Down
28 changes: 28 additions & 0 deletions tests/async/library/test_har.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,34 @@ async def test_HA_021_should_include_content_smoke(
assert content["compression"] == 0


async def test_HA_113_should_store_binary_content_with_a_textual_mime_type_as_base64(
page_with_har: PageWithHar, server: Server, browser_name: str
) -> None:
if browser_name == "webkit":
pytest.skip(
"fixme: WebKit decodes the body of a textual resource before it gets to us"
)
body = bytes([0x80, 0xC3, 0x28, 0xFF, 0x41])

def handle(request: TestServerRequest) -> None:
request.setHeader("Content-Type", "application/javascript")
request.write(body)
request.finish()

server.set_route("/binary.js", handle)
har = await page_with_har()
await har.page.goto(server.EMPTY_PAGE)
await har.page.evaluate("() => fetch('/binary.js').then(r => r.arrayBuffer())")
log = await har.get_log()
content = next(
entry
for entry in log["entries"]
if entry["request"]["url"].endswith("/binary.js")
)["response"]["content"]
assert content["encoding"] == "base64"
assert base64.b64decode(content["text"]) == body


async def test_HA_022_should_use_attach_mode_for_zip_extension(
page_with_har: PageWithHar, server: Server
) -> None:
Expand Down
41 changes: 37 additions & 4 deletions tests/async/page/test_page_drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,6 @@ async def test_DD_008_drag_and_drop_should_respect_the_drop_effect(
is_mac: bool,
headless: bool,
) -> None:
if browser_name == "webkit" and not is_linux:
pytest.skip(
"fixme: WebKit doesn't handle the drop effect correctly outside of linux."
)
if browser_name == "webkit" and is_linux and not headless:
pytest.skip("fixme: https://github.com/microsoft/playwright/issues/21646")
if browser_name == "chromium" and not is_mac and not headless:
Expand Down Expand Up @@ -340,6 +336,43 @@ async def test_if_dropped(effect_allowed: str, drop_effect: str) -> Any:
assert await test_if_dropped("uninitialized", "copy") is True


async def test_DD_028_drag_and_drop_should_report_the_default_drop_effect_from_effect_allowed(
page: Page, browser_name: str, is_linux: bool, headless: bool
) -> None:
if browser_name == "webkit" and is_linux and not headless:
pytest.skip("fixme: https://github.com/microsoft/playwright/issues/21646")

await page.set_content(
"""
<div draggable="true">drag target</div>
<drop-target>this is the drop target</drop-target>
"""
)
await page.evaluate(
"""() => {
window['dropEffect'] = 'none';

document.querySelector('div').addEventListener('dragstart', event => {
event.dataTransfer.effectAllowed = 'copyMove';
event.dataTransfer.setData('text/plain', 'drag data');
});
document.querySelector('div').addEventListener('dragend', event => {
window['dropEffect'] = event.dataTransfer.dropEffect;
});

// The target accepts the drag without setting dropEffect.
document.querySelector('drop-target').addEventListener('dragover', event => {
event.preventDefault();
});
}"""
)
await page.hover("div")
await page.mouse.down()
await page.hover("drop-target")
await page.mouse.up()
assert await page.evaluate("dropEffect") == "move"


async def test_DD_009_drag_and_drop_should_work_if_the_drag_is_canceled(
page: Page, server: Server
) -> None:
Expand Down
5 changes: 4 additions & 1 deletion tests/async/page/test_page_fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,12 +412,15 @@ async def test_FI_025_should_be_able_to_fill_when_focus_is_in_the_wrong_frame(
await page.set_content(
"""
<div contentEditable="true"></div>
<iframe></iframe>
<iframe srcdoc="<body contenteditable><p>old</p></body>"></iframe>
"""
)
await page.focus("iframe")
await page.fill("div", "some value")
assert await page.eval_on_selector("div", "d => d.textContent") == "some value"
paragraph = page.frame_locator("iframe").locator("p")
await paragraph.fill("new")
await expect(paragraph).to_have_text("new")


async def test_FI_026_should_be_able_to_fill_the_input_type_number(
Expand Down
12 changes: 6 additions & 6 deletions tests/async/page/test_page_mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,17 +359,17 @@ async def test_MO_012_should_tween_mouse_movement(
"""() => {
window['result'] = [];
document.addEventListener('mousemove', event => {
window['result'].push([event.clientX, event.clientY]);
window['result'].push([event.clientX, event.clientY, event.movementX, event.movementY]);
});
}"""
)
await page.mouse.move(200, 300, steps=5)
assert await page.evaluate("result") == [
[120, 140],
[140, 180],
[160, 220],
[180, 260],
[200, 300],
[120, 140, 20, 40],
[140, 180, 20, 40],
[160, 220, 20, 40],
[180, 260, 20, 40],
[200, 300, 20, 40],
]


Expand Down
19 changes: 15 additions & 4 deletions tests/async/page/test_page_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -1220,16 +1220,27 @@ async def test_RO_044_should_support_cors_for_different_methods(
async def test_RO_045_should_support_the_times_parameter_with_route_matching(
page: Page, server: Server
) -> None:
await page.goto(server.EMPTY_PAGE)

async def fetch_body() -> str:
return await page.evaluate(
"() => fetch('/empty.html').then(response => response.text())"
)

intercepted: List[int] = []

async def handler(route: Route) -> None:
intercepted.append(1)
await route.continue_()
await route.fulfill(body="first")

await page.route("**/empty.html", handler, times=1)
await page.goto(server.EMPTY_PAGE)
await page.goto(server.EMPTY_PAGE)
await page.goto(server.EMPTY_PAGE)
assert await fetch_body() == "first"
await page.route(
"**/empty.html", lambda route: route.fulfill(body="second"), times=1
)
assert await fetch_body() == "second"
await fetch_body()
await fetch_body()
assert len(intercepted) == 1


Expand Down
38 changes: 38 additions & 0 deletions tests/async/page/test_page_screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,44 @@ async def test_SS_025_page_screenshot_should_work_for_webgl(
assert_to_be_golden(screenshot, "screenshot-webgl.png")


@pytest.mark.usefixtures("skip_firefox_headed")
async def test_SS_095_page_screenshot_should_capture_webgl_canvas_after_its_drawing_buffer_is_resized(
page: Page, browser_name: str, platform: str, utils: Utils
) -> None:
if (
browser_name == "webkit"
and platform == "darwin"
and os.uname().machine == "x86_64"
):
pytest.skip(
"WebGL is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277"
)

await page.set_viewport_size({"width": 200, "height": 200})
await page.set_content(
'<body style="margin:0"><div style="width:100px;height:100px;background:rgb(0,255,0)"></div></body>'
)
expected = await page.screenshot()

await page.set_content(
"""<body style="margin:0"><canvas style="width:100px;height:100px"></canvas><script>
const gl = document.querySelector('canvas').getContext('webgl');
window.draw = size => {
gl.canvas.width = size;
gl.canvas.height = size;
gl.viewport(0, 0, size, size);
gl.clearColor(0, 1, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
};
draw(100);
</script></body>"""
)
await utils.rafraf(page)
await page.evaluate("() => window.draw(50)")
await utils.rafraf(page)
assert compare_images(await page.screenshot(), expected) is None


@pytest.mark.usefixtures("skip_firefox_headed")
async def test_SS_026_page_screenshot_should_work_for_translate_z(
page: Page, server: Server, assert_to_be_golden: GoldenAssert
Expand Down
Loading
Loading