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 @@
f33f0b751be0dd559d86c1ed27ca8baa5f9cb2b2
eac236da288cdc3d0891278a61e1a876348950a0
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->157.0.8103.15<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->158.0.8127.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->27.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->158.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

Expand Down
32 changes: 30 additions & 2 deletions playwright/_impl/clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,27 @@
"""

import datetime
from typing import TYPE_CHECKING, Optional, TypedDict, Union

import math
from typing import (
TYPE_CHECKING,
Awaitable,
Callable,
Optional,
TypedDict,
TypeVar,
Union,
)

from playwright._impl.channel_owner import ApiCallOptions, ApiZone, api_call
from playwright._impl.errors import Error
from playwright._impl.isomorphic.string_utils import js_string
from playwright._impl.timeout_settings import NO_TIMEOUT

if TYPE_CHECKING: # pragma: no cover
from playwright._impl.browser_context import BrowserContext

R = TypeVar("R")


class Clock:
def __init__(self, browser_context: "BrowserContext") -> None:
Expand All @@ -36,6 +50,14 @@ def __init__(self, browser_context: "BrowserContext") -> None:
self._loop = browser_context._loop
self._dispatcher_fiber = browser_context._dispatcher_fiber

def _wrap_api_call(
self,
func: Callable[[ApiZone], Awaitable[R]],
options: Optional[ApiCallOptions] = None,
) -> Awaitable[R]:
# Python: include the API name in errors raised before the channel call.
return self._browser_context._wrap_api_call(func, options)

async def install(
self, time: Optional[Union[float, str, datetime.datetime]]
) -> None:
Expand All @@ -46,6 +68,7 @@ async def install(
timeout=NO_TIMEOUT,
)

@api_call()
async def fast_forward(self, ticks: Union[int, str]) -> None:
params = parse_ticks(ticks)
await self._browser_context._channel.clockFastForward(
Expand All @@ -65,6 +88,7 @@ async def pause_at(self, time: Union[float, str, datetime.datetime]) -> None:
async def resume(self) -> None:
await self._browser_context._channel.clockResume(timeout=NO_TIMEOUT)

@api_call()
async def run_for(self, ticks: Union[int, str]) -> None:
params = parse_ticks(ticks)
await self._browser_context._channel.clockRunFor(
Expand Down Expand Up @@ -113,6 +137,10 @@ def parse_time(time: Union[float, str, datetime.datetime]) -> _TimeParams:


def parse_ticks(ticks: Union[int, str]) -> _TicksParams:
# Python: reject non-finite ticks before JSON serialization; upstream rejects
# them in the server, but JSON cannot carry NaN or Infinity to the driver.
if isinstance(ticks, float) and not math.isfinite(ticks):
raise Error(f"Invalid ticks: {js_string(ticks)}")
return {
"ticksNumber": ticks if isinstance(ticks, (int, float)) else None,
"ticksString": ticks if isinstance(ticks, str) else None,
Expand Down
10 changes: 6 additions & 4 deletions playwright/async_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -4675,8 +4675,9 @@ async def fast_forward(self, ticks: typing.Union[int, str]) -> None:
Parameters
----------
ticks : Union[int, str]
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
"08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
Time may be a finite number of milliseconds to advance the clock by or a human-readable string. Valid string
formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten
seconds.
"""

await self._impl_obj.fast_forward(ticks=ticks)
Expand Down Expand Up @@ -4772,8 +4773,9 @@ async def run_for(self, ticks: typing.Union[int, str]) -> None:
Parameters
----------
ticks : Union[int, str]
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
"08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
Time may be a finite number of milliseconds to advance the clock by or a human-readable string. Valid string
formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten
seconds.
"""

await self._impl_obj.run_for(ticks=ticks)
Expand Down
10 changes: 6 additions & 4 deletions playwright/sync_api/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -4585,8 +4585,9 @@ def fast_forward(self, ticks: typing.Union[int, str]) -> None:
Parameters
----------
ticks : Union[int, str]
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
"08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
Time may be a finite number of milliseconds to advance the clock by or a human-readable string. Valid string
formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten
seconds.
"""

self._sync(self._impl_obj.fast_forward(ticks=ticks))
Expand Down Expand Up @@ -4682,8 +4683,9 @@ def run_for(self, ticks: typing.Union[int, str]) -> None:
Parameters
----------
ticks : Union[int, str]
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
"08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
Time may be a finite number of milliseconds to advance the clock by or a human-readable string. Valid string
formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten
seconds.
"""

self._sync(self._impl_obj.run_for(ticks=ticks))
Expand Down
4 changes: 4 additions & 0 deletions tests/assets/serviceworkers/fetchdummy/sw.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('/headers')) {
event.respondWith(new Response(JSON.stringify(Object.fromEntries(event.request.headers))));
return;
}
if (event.request.url.endsWith('.html') || event.request.url.includes('passthrough')) {
event.respondWith(fetch(event.request));
return;
Expand Down
18 changes: 18 additions & 0 deletions tests/async/library/test_browsercontext_har.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,24 @@ async def test_HA_085_should_round_trip_har_zip(
"background-color", "rgb(255, 192, 203)"
)

# Python: zipfile builds the in-memory archive instead of yazl's output stream.
output = io.BytesIO()
with zipfile.ZipFile(output, "w") as zip_file:
for entry in har_dir.iterdir():
zip_file.write(
entry, "Recording.HAR" if entry.name == "har.har" else entry.name
)
uppercase_har_path = tmp_path / "uppercase.har.zip"
uppercase_har_path.write_bytes(output.getvalue())

await context2.unroute_all(behavior="wait")
await context2.route_from_har(uppercase_har_path, not_found="abort")
await page2.reload()
assert "hello, world!" in await page2.content()
await expect(page2.locator("body")).to_have_css(
"background-color", "rgb(255, 192, 203)"
)


async def test_HA_114_should_reject_a_malformed_har_zip(
context_factory: Callable[..., Awaitable[BrowserContext]], tmp_path: Path
Expand Down
22 changes: 21 additions & 1 deletion tests/async/library/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,13 +620,33 @@ async def handle(route: Route) -> None:


async def test_CA_029_should_not_crash_on_feature_detection_for_public_key_credential(
page: Page, server: Server
page: Page, server: Server, context: BrowserContext
) -> None:
await page.goto(server.EMPTY_PAGE)
assert await page.evaluate("() => typeof PublicKeyCredential") == "function"
assert (
await page.evaluate("() => PublicKeyCredential.name") == "PublicKeyCredential"
)
assert await page.evaluate("() => ({}) instanceof PublicKeyCredential") is False
assert (
await page.evaluate(
"""() => {
try {
new PublicKeyCredential();
return false;
} catch (e) {
return e instanceof TypeError;
}
}"""
)
is True
)
await page.evaluate(
"""async () => {
await PublicKeyCredential.getClientCapabilities();
await PublicKeyCredential.isConditionalMediationAvailable();
await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}"""
)
await context.credentials.install()
assert await page.evaluate("() => typeof PublicKeyCredential") == "function"
19 changes: 15 additions & 4 deletions tests/async/library/test_page_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ async def test_CK_011_run_for_passes_2_hours_34_minutes_and_10_seconds(

@pytest.mark.usefixtures("install_and_pause")
async def test_CK_012_run_for_throws_for_invalid_format(
page: Page, calls: Calls
page: Page, calls: Calls, server: Server
) -> None:
await page.evaluate(
"""async () => {
Expand All @@ -234,7 +234,13 @@ async def test_CK_012_run_for_throws_for_invalid_format(
)
with pytest.raises(Error):
await page.clock.run_for("12:02:34:10")
with pytest.raises(Error, match="Clock.run_for: Invalid ticks: NaN"):
await page.clock.run_for(float("nan")) # type: ignore[arg-type]
with pytest.raises(Error, match="Clock.fast_forward: Invalid ticks: Infinity"):
await page.clock.fast_forward(float("inf")) # type: ignore[arg-type]
assert calls == []
await page.goto(server.EMPTY_PAGE)
assert await page.evaluate("() => Date.now()") == 1000


@pytest.mark.usefixtures("install_and_pause")
Expand Down Expand Up @@ -262,17 +268,22 @@ async def test_CK_014_fast_forward_ignores_timers_which_wouldn_t_be_run(

@pytest.mark.usefixtures("install_and_pause")
async def test_CK_015_fast_forward_pushes_back_execution_time_for_skipped_timers(
page: Page, calls: Calls
page: Page, calls: Calls, server: Server
) -> None:
await page.goto(server.EMPTY_PAGE)
await page.evaluate(
"""async () => {
setTimeout(() => {
window.stub(Date.now());
}, 1000);
}"""
)
await page.clock.fast_forward(2000)
assert calls == [{"params": [1000 + 2000]}]
fifty_days = 50 * 24 * 60 * 60 * 1000
await page.clock.fast_forward(fifty_days)
assert calls == [{"params": [1000 + fifty_days]}]
assert await page.evaluate("() => Date.now()") == 1000 + fifty_days
await page.reload()
assert await page.evaluate("() => Date.now()") == 1000 + fifty_days


@pytest.mark.usefixtures("install_and_pause")
Expand Down
12 changes: 11 additions & 1 deletion tests/async/library/test_page_close.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,24 @@ async def handler() -> None:


async def test_PA_042_should_reject_all_promises_when_page_is_closed(
page: Page,
page: Page, server: Server
) -> None:
server.set_route("/hang", lambda request: None)
request_promise = asyncio.create_task(server.wait_for_request("/hang"))
await page.set_content(
f'<iframe src="{server.PREFIX}/hang"></iframe>', wait_until="domcontentloaded"
)
await request_promise
frame_error_promise = asyncio.create_task(
error_of(page.frames[1].evaluate("() => new Promise(() => {})"))
)
error, _ = await asyncio.gather(
error_of(page.evaluate("() => new Promise(r => {})")),
page.close(),
)
assert isinstance(error, Error)
assert TARGET_CLOSED_ERROR_MESSAGE in error.message
assert TARGET_CLOSED_ERROR_MESSAGE in (await frame_error_promise).message


async def test_PA_043_should_set_the_page_close_state(page: Page) -> None:
Expand Down
14 changes: 14 additions & 0 deletions tests/async/library/test_tap.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ async def test_TA_001_should_send_all_of_the_correct_events_smoke(page: Page) ->
"click",
]

await events_handle.evaluate("events => events.length = 0")
await page.locator("#b").evaluate(
"""target => {
target.addEventListener('pointerdown', event => event.preventDefault(), { once: true });
}"""
)
await page.tap("#b")
events = [
event
for event in await events_handle.json_value()
if event in ["pointerdown", "pointerup", "mousedown", "mouseup", "click"]
]
assert events == ["pointerdown", "pointerup", "click"]


async def test_TA_002_trial_run_should_not_tap(page: Page) -> None:
await page.set_content(
Expand Down
18 changes: 17 additions & 1 deletion tests/async/page/test_interception.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def handler(route: Route) -> None:


async def test_RO_152_should_intercept_after_a_service_worker(
page: Page, server: Server, browser_name: str
page: Page, server: Server, browser_name: str, is_frozen_webkit: bool
) -> None:
await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html")
await page.evaluate("() => window['activationPromise']")
Expand All @@ -76,6 +76,22 @@ async def handler(route: Route) -> None:
sw_response2 = await page.evaluate("() => window['fetchDummy']('foo')")
assert sw_response2 == "responseFromServiceWorker:foo"

# Frozen WebKit predates the header preservation fix.
if not is_frozen_webkit:
headers = {
"cache-control": "max-age=12345",
"accept-language": "fr-CH",
"accept": "application/custom",
}
received_headers = await page.evaluate(
"""async headers => {
const response = await fetch('headers', { headers });
return response.json();
}""",
headers,
)
assert received_headers.items() >= headers.items()

# Page route is not applied to service worker initiated fetch.
non_intercepted_response = await page.evaluate(
"() => window['fetchDummy']('passthrough')"
Expand Down
Loading
Loading