diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index 5a930f1e6..4db0e2635 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -13,8 +13,8 @@ # limitations under the License. import collections.abc +import datetime import math -from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from urllib.parse import ParseResult, urlparse, urlunparse @@ -128,8 +128,13 @@ def serialize_value( return dict(v="-0") if math.isnan(value): return dict(v="NaN") - if isinstance(value, datetime): - return dict(d=value.isoformat() + "Z") + if isinstance(value, datetime.datetime): + # Node.js Date objects are always in UTC. + return { + "d": datetime.datetime.strftime( + value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ" + ) + } if isinstance(value, bool): return {"b": value} if isinstance(value, (int, float)): @@ -205,7 +210,10 @@ def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: return a if "d" in value: - return datetime.fromisoformat(value["d"][:-1]) + # Node.js Date objects are always in UTC. + return datetime.datetime.strptime( + value["d"], "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=datetime.timezone.utc) if "o" in value: o: Dict = {} diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 774d60de5..b8936f4bf 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -13,8 +13,8 @@ # limitations under the License. import asyncio +import datetime import re -from datetime import datetime import pytest @@ -183,7 +183,11 @@ async def test_assertions_locator_to_have_js_property( ) await expect(page.locator("div")).to_have_js_property( "foo", - {"a": 1, "b": "string", "c": datetime.utcfromtimestamp(1627503992000 / 1000)}, + { + "a": 1, + "b": "string", + "c": datetime.datetime.fromtimestamp(1627503992000 / 1000), + }, ) diff --git a/tests/async/test_evaluate.py b/tests/async/test_evaluate.py index cafeac61d..0b2143769 100644 --- a/tests/async/test_evaluate.py +++ b/tests/async/test_evaluate.py @@ -13,7 +13,7 @@ # limitations under the License. import math -from datetime import datetime +from datetime import datetime, timedelta, timezone from typing import Optional from urllib.parse import ParseResult, urlparse @@ -216,17 +216,38 @@ async def test_evaluate_evaluate_date(page: Page) -> None: result = await page.evaluate( '() => ({ date: new Date("2020-05-27T01:31:38.506Z") })' ) - assert result == {"date": datetime.fromisoformat("2020-05-27T01:31:38.506")} + assert result == { + "date": datetime.fromisoformat("2020-05-27T01:31:38.506").replace( + tzinfo=timezone.utc + ) + } + + +async def test_evaluate_roundtrip_date_without_tzinfo(page: Page) -> None: + date = datetime.fromisoformat("2020-05-27T01:31:38.506") + result = await page.evaluate("date => date", date) + assert result.timestamp() == date.timestamp() async def test_evaluate_roundtrip_date(page: Page) -> None: + date = datetime.fromisoformat("2020-05-27T01:31:38.506").replace( + tzinfo=timezone.utc + ) + result = await page.evaluate("date => date", date) + assert result == date + + +async def test_evaluate_roundtrip_date_with_tzinfo(page: Page) -> None: date = datetime.fromisoformat("2020-05-27T01:31:38.506") + date = date.astimezone(timezone(timedelta(hours=4))) result = await page.evaluate("date => date", date) assert result == date async def test_evaluate_jsonvalue_date(page: Page) -> None: - date = datetime.fromisoformat("2020-05-27T01:31:38.506") + date = datetime.fromisoformat("2020-05-27T01:31:38.506").replace( + tzinfo=timezone.utc + ) result = await page.evaluate( '() => ({ date: new Date("2020-05-27T01:31:38.506Z") })' ) diff --git a/tests/async/test_jshandle.py b/tests/async/test_jshandle.py index f4136e92c..f18cbd633 100644 --- a/tests/async/test_jshandle.py +++ b/tests/async/test_jshandle.py @@ -14,7 +14,7 @@ import json import math -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict from playwright.async_api import Page @@ -180,7 +180,9 @@ async def test_jshandle_json_value_work(page: Page) -> None: async def test_jshandle_json_value_work_with_dates(page: Page) -> None: handle = await page.evaluate_handle('() => new Date("2020-05-27T01:31:38.506Z")') json = await handle.json_value() - assert json == datetime.fromisoformat("2020-05-27T01:31:38.506") + assert json == datetime.fromisoformat("2020-05-27T01:31:38.506").replace( + tzinfo=timezone.utc + ) async def test_jshandle_json_value_should_work_for_circular_object(page: Page) -> None: diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index f2df44ab5..d7180fc94 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import re -from datetime import datetime import pytest @@ -163,7 +163,11 @@ def test_assertions_locator_to_have_js_property(page: Page, server: Server) -> N ) expect(page.locator("div")).to_have_js_property( "foo", - {"a": 1, "b": "string", "c": datetime.utcfromtimestamp(1627503992000 / 1000)}, + { + "a": 1, + "b": "string", + "c": datetime.datetime.fromtimestamp(1627503992000 / 1000), + }, )