Skip to content

Commit 812f8f9

Browse files
authored
fix: datetime parsing with non-UTC timezones (#2322)
1 parent 1a68508 commit 812f8f9

File tree

5 files changed

+52
-13
lines changed

5 files changed

+52
-13
lines changed

playwright/_impl/_js_handle.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
import collections.abc
16+
import datetime
1617
import math
17-
from datetime import datetime
1818
from pathlib import Path
1919
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
2020
from urllib.parse import ParseResult, urlparse, urlunparse
@@ -128,8 +128,13 @@ def serialize_value(
128128
return dict(v="-0")
129129
if math.isnan(value):
130130
return dict(v="NaN")
131-
if isinstance(value, datetime):
132-
return dict(d=value.isoformat() + "Z")
131+
if isinstance(value, datetime.datetime):
132+
# Node.js Date objects are always in UTC.
133+
return {
134+
"d": datetime.datetime.strftime(
135+
value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ"
136+
)
137+
}
133138
if isinstance(value, bool):
134139
return {"b": value}
135140
if isinstance(value, (int, float)):
@@ -205,7 +210,10 @@ def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any:
205210
return a
206211

207212
if "d" in value:
208-
return datetime.fromisoformat(value["d"][:-1])
213+
# Node.js Date objects are always in UTC.
214+
return datetime.datetime.strptime(
215+
value["d"], "%Y-%m-%dT%H:%M:%S.%fZ"
216+
).replace(tzinfo=datetime.timezone.utc)
209217

210218
if "o" in value:
211219
o: Dict = {}

tests/async/test_assertions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
import asyncio
16+
import datetime
1617
import re
17-
from datetime import datetime
1818

1919
import pytest
2020

@@ -183,7 +183,11 @@ async def test_assertions_locator_to_have_js_property(
183183
)
184184
await expect(page.locator("div")).to_have_js_property(
185185
"foo",
186-
{"a": 1, "b": "string", "c": datetime.utcfromtimestamp(1627503992000 / 1000)},
186+
{
187+
"a": 1,
188+
"b": "string",
189+
"c": datetime.datetime.fromtimestamp(1627503992000 / 1000),
190+
},
187191
)
188192

189193

tests/async/test_evaluate.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import math
16-
from datetime import datetime
16+
from datetime import datetime, timedelta, timezone
1717
from typing import Optional
1818
from urllib.parse import ParseResult, urlparse
1919

@@ -216,17 +216,38 @@ async def test_evaluate_evaluate_date(page: Page) -> None:
216216
result = await page.evaluate(
217217
'() => ({ date: new Date("2020-05-27T01:31:38.506Z") })'
218218
)
219-
assert result == {"date": datetime.fromisoformat("2020-05-27T01:31:38.506")}
219+
assert result == {
220+
"date": datetime.fromisoformat("2020-05-27T01:31:38.506").replace(
221+
tzinfo=timezone.utc
222+
)
223+
}
224+
225+
226+
async def test_evaluate_roundtrip_date_without_tzinfo(page: Page) -> None:
227+
date = datetime.fromisoformat("2020-05-27T01:31:38.506")
228+
result = await page.evaluate("date => date", date)
229+
assert result.timestamp() == date.timestamp()
220230

221231

222232
async def test_evaluate_roundtrip_date(page: Page) -> None:
233+
date = datetime.fromisoformat("2020-05-27T01:31:38.506").replace(
234+
tzinfo=timezone.utc
235+
)
236+
result = await page.evaluate("date => date", date)
237+
assert result == date
238+
239+
240+
async def test_evaluate_roundtrip_date_with_tzinfo(page: Page) -> None:
223241
date = datetime.fromisoformat("2020-05-27T01:31:38.506")
242+
date = date.astimezone(timezone(timedelta(hours=4)))
224243
result = await page.evaluate("date => date", date)
225244
assert result == date
226245

227246

228247
async def test_evaluate_jsonvalue_date(page: Page) -> None:
229-
date = datetime.fromisoformat("2020-05-27T01:31:38.506")
248+
date = datetime.fromisoformat("2020-05-27T01:31:38.506").replace(
249+
tzinfo=timezone.utc
250+
)
230251
result = await page.evaluate(
231252
'() => ({ date: new Date("2020-05-27T01:31:38.506Z") })'
232253
)

tests/async/test_jshandle.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import json
1616
import math
17-
from datetime import datetime
17+
from datetime import datetime, timezone
1818
from typing import Any, Dict
1919

2020
from playwright.async_api import Page
@@ -180,7 +180,9 @@ async def test_jshandle_json_value_work(page: Page) -> None:
180180
async def test_jshandle_json_value_work_with_dates(page: Page) -> None:
181181
handle = await page.evaluate_handle('() => new Date("2020-05-27T01:31:38.506Z")')
182182
json = await handle.json_value()
183-
assert json == datetime.fromisoformat("2020-05-27T01:31:38.506")
183+
assert json == datetime.fromisoformat("2020-05-27T01:31:38.506").replace(
184+
tzinfo=timezone.utc
185+
)
184186

185187

186188
async def test_jshandle_json_value_should_work_for_circular_object(page: Page) -> None:

tests/sync/test_assertions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import datetime
1516
import re
16-
from datetime import datetime
1717

1818
import pytest
1919

@@ -163,7 +163,11 @@ def test_assertions_locator_to_have_js_property(page: Page, server: Server) -> N
163163
)
164164
expect(page.locator("div")).to_have_js_property(
165165
"foo",
166-
{"a": 1, "b": "string", "c": datetime.utcfromtimestamp(1627503992000 / 1000)},
166+
{
167+
"a": 1,
168+
"b": "string",
169+
"c": datetime.datetime.fromtimestamp(1627503992000 / 1000),
170+
},
167171
)
168172

169173

0 commit comments

Comments
 (0)