Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fe34e0f
Started roll
agg23 Jun 16, 2025
1a85b74
In progress changes from v1.53.0 release
agg23 Jun 17, 2025
ba40171
Initial compiling roll to 1.53.0-alpha-2025-05-21
agg23 Jun 17, 2025
05ce9bc
Revert accidental removal of API wrapping
agg23 Jun 17, 2025
13d5bb5
More timeout handling
agg23 Jun 17, 2025
a78c02e
Fixed overeager timeout conversion in Page
agg23 Jun 18, 2025
a626c55
Switch to 2025-06-07 and fix cookies
agg23 Jun 18, 2025
5c82385
Bumped to 1.53.0-beta-1749221468000 due to selector bug
agg23 Jun 18, 2025
e9c491b
Fix launch_persistent_context deserialization
agg23 Jun 19, 2025
251c231
Remove superfluous BrowserContext addition
agg23 Jun 19, 2025
2bdd016
Update tracing tests to new log format
agg23 Jun 19, 2025
5ad2780
More timeout operation fixes
agg23 Jun 19, 2025
609274d
Fix HAR path interpretation
agg23 Jun 19, 2025
39da953
Fixed context options and video tests
agg23 Jun 19, 2025
bbdbfe3
Fix selector registration
agg23 Jun 20, 2025
d073877
Clean up HAR option serialization
agg23 Jun 20, 2025
7d76c32
Dummy logging for CI debugging
agg23 Jun 20, 2025
87dbb04
Add context logging in Selectors.register
agg23 Jun 20, 2025
3577f98
More logging
agg23 Jun 20, 2025
04643aa
Log browser context at start of failing test
agg23 Jun 20, 2025
3af6375
Log context close
agg23 Jun 20, 2025
88cc81e
Explicitly close contexts in sync tests that were missing it
agg23 Jun 20, 2025
a3b1157
Logging all contexts when closing one
agg23 Jun 20, 2025
aac337b
More selector context logging
agg23 Jun 20, 2025
2a62bcd
Switch context collections to sets due to duplicate events
agg23 Jun 20, 2025
3e42fbd
Clean up debug information
agg23 Jun 20, 2025
2c3f2c6
Fix strange Literal import
agg23 Jun 20, 2025
e676ccb
Regenerate APIs
agg23 Jun 20, 2025
80a86c7
Remove accidental print statements
agg23 Jun 20, 2025
477e78b
Minor PR cleanup
agg23 Jun 23, 2025
576871d
Normalized timeout setting
agg23 Jun 23, 2025
1f240e4
Remove unused method
agg23 Jun 23, 2025
9dc4609
Proper assertion titles
agg23 Jun 23, 2025
4c7519c
Add missing assertion tests
agg23 Jun 23, 2025
4eeeaeb
Double click action title
agg23 Jun 23, 2025
7ed6587
Roll to 1.53.1
agg23 Jun 23, 2025
f52748a
viewportSizeChanged event from #35994
agg23 Jun 23, 2025
4eb72f6
Add missing new_page() title
agg23 Jun 23, 2025
e4a4a3c
Fix extra self in _click() inner method
agg23 Jun 24, 2025
587355e
snake_case fix
agg23 Jun 24, 2025
b3e5199
Add missing `_browser_type` instance variable
agg23 Jun 24, 2025
8f9d4c3
Add browser_type assertions
agg23 Jun 24, 2025
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
6 changes: 3 additions & 3 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 -->136.0.7103.25<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->18.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->137.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->138.0.7204.15<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->18.5<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->139.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |

## Documentation

Expand Down
102 changes: 53 additions & 49 deletions playwright/_impl/_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json
from pathlib import Path
from types import SimpleNamespace
from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast
from typing import (
TYPE_CHECKING,
Dict,
List,
Optional,
Pattern,
Sequence,
Set,
Union,
cast,
)

from playwright._impl._api_structures import (
ClientCertificate,
Expand All @@ -38,12 +47,9 @@
HarMode,
ReducedMotion,
ServiceWorkersPolicy,
async_readfile,
locals_to_params,
make_dirs_for_file,
prepare_record_har_options,
)
from playwright._impl._network import serialize_headers, to_client_certificates_protocol
from playwright._impl._page import Page

if TYPE_CHECKING: # pragma: no cover
Expand All @@ -64,20 +70,52 @@ def __init__(
self._should_close_connection_on_close = False
self._cr_tracing_path: Optional[str] = None

self._contexts: List[BrowserContext] = []
self._contexts: Set[BrowserContext] = set()
self._traces_dir: Optional[str] = None
self._channel.on(
"context",
lambda params: self._did_create_context(
cast(BrowserContext, from_channel(params["context"]))
),
)
self._channel.on("close", lambda _: self._on_close())
self._close_reason: Optional[str] = None

def __repr__(self) -> str:
return f"<Browser type={self._browser_type} version={self.version}>"

def _connect_to_browser_type(
self,
browserType: "BrowserType",
tracesDir: Optional[str] = None,
) -> None:
# Note: when using connect(), `browserType` is different from `this.parent`.
# This is why browser type is not wired up in the constructor, and instead this separate method is called later on.
self._browser_type = browserType
self._traces_dir = tracesDir
for context in self._contexts:
self._setup_browser_context(context)

def _did_create_context(self, context: BrowserContext) -> None:
context._browser = self
self._contexts.add(context)
# Note: when connecting to a browser, initial contexts arrive before `_browserType` is set,
# and will be configured later in `ConnectToBrowserType`.
if self._browser_type:
self._setup_browser_context(context)

def _setup_browser_context(self, context: BrowserContext) -> None:
context._tracing._traces_dir = self._traces_dir
print("Appending context to selectors")
self._browser_type._playwright.selectors._contextsForSelectors.add(context)

def _on_close(self) -> None:
self._is_connected = False
self.emit(Browser.Events.Disconnected, self)

@property
def contexts(self) -> List[BrowserContext]:
return self._contexts.copy()
return list(self._contexts)

@property
def browser_type(self) -> "BrowserType":
Expand Down Expand Up @@ -126,11 +164,17 @@ async def new_context(
clientCertificates: List[ClientCertificate] = None,
) -> BrowserContext:
params = locals_to_params(locals())
await prepare_browser_context_params(params)
await self._browser_type._prepare_browser_context_params(params)

channel = await self._channel.send("newContext", params)
context = cast(BrowserContext, from_channel(channel))
self._browser_type._did_create_context(context, params, {})
await context._initialize_har_from_options(
record_har_content=recordHarContent,
record_har_mode=recordHarMode,
record_har_omit_content=recordHarOmitContent,
record_har_path=recordHarPath,
record_har_url_filter=recordHarUrlFilter,
)
return context

async def new_page(
Expand Down Expand Up @@ -226,43 +270,3 @@ async def stop_tracing(self) -> bytes:
f.write(buffer)
self._cr_tracing_path = None
return buffer


async def prepare_browser_context_params(params: Dict) -> None:
if params.get("noViewport"):
del params["noViewport"]
params["noDefaultViewport"] = True
if "defaultBrowserType" in params:
del params["defaultBrowserType"]
if "extraHTTPHeaders" in params:
params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"])
if "recordHarPath" in params:
params["recordHar"] = prepare_record_har_options(params)
del params["recordHarPath"]
if "recordVideoDir" in params:
params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()}
if "recordVideoSize" in params:
params["recordVideo"]["size"] = params["recordVideoSize"]
del params["recordVideoSize"]
del params["recordVideoDir"]
if "storageState" in params:
storageState = params["storageState"]
if not isinstance(storageState, dict):
params["storageState"] = json.loads(
(await async_readfile(storageState)).decode()
)
if params.get("colorScheme", None) == "null":
params["colorScheme"] = "no-override"
if params.get("reducedMotion", None) == "null":
params["reducedMotion"] = "no-override"
if params.get("forcedColors", None) == "null":
params["forcedColors"] = "no-override"
if params.get("contrast", None) == "null":
params["contrast"] = "no-override"
if "acceptDownloads" in params:
params["acceptDownloads"] = "accept" if params["acceptDownloads"] else "deny"

if "clientCertificates" in params:
params["clientCertificates"] = await to_client_certificates_protocol(
params["clientCertificates"]
)
92 changes: 57 additions & 35 deletions playwright/_impl/_browser_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
async_writefile,
locals_to_params,
parse_error,
prepare_record_har_options,
to_impl,
)
from playwright._impl._network import (
Expand Down Expand Up @@ -106,18 +105,18 @@ def __init__(
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
) -> None:
super().__init__(parent, type, guid, initializer)
# Browser is null for browser contexts created outside of normal browser, e.g. android or electron.
# circular import workaround:
self._browser: Optional["Browser"] = None
if parent.__class__.__name__ == "Browser":
self._browser = cast("Browser", parent)
self._browser._contexts.append(self)
self._pages: List[Page] = []
self._routes: List[RouteHandler] = []
self._web_socket_routes: List[WebSocketRouteHandler] = []
self._bindings: Dict[str, Any] = {}
self._timeout_settings = TimeoutSettings(None)
self._owner_page: Optional[Page] = None
self._options: Dict[str, Any] = {}
self._options: Dict[str, Any] = initializer.get("options", {})
self._background_pages: Set[Page] = set()
self._service_workers: Set[Worker] = set()
self._tracing = cast(Tracing, from_channel(initializer["tracing"]))
Expand Down Expand Up @@ -220,7 +219,7 @@ def __init__(
BrowserContext.Events.RequestFailed: "requestFailed",
}
)
self._close_was_called = False
self._closing_or_closed = False

def __repr__(self) -> str:
return f"<BrowserContext browser={self.browser}>"
Expand All @@ -237,7 +236,7 @@ async def _on_route(self, route: Route) -> None:
route_handlers = self._routes.copy()
for route_handler in route_handlers:
# If the page or the context was closed we stall all requests right away.
if (page and page._close_was_called) or self._close_was_called:
if (page and page._close_was_called) or self._closing_or_closed:
return
if not route_handler.matches(route.request.url):
continue
Expand Down Expand Up @@ -288,19 +287,12 @@ def set_default_navigation_timeout(self, timeout: float) -> None:

def _set_default_navigation_timeout_impl(self, timeout: Optional[float]) -> None:
self._timeout_settings.set_default_navigation_timeout(timeout)
self._channel.send_no_reply(
"setDefaultNavigationTimeoutNoReply",
{} if timeout is None else {"timeout": timeout},
)

def set_default_timeout(self, timeout: float) -> None:
return self._set_default_timeout_impl(timeout)

def _set_default_timeout_impl(self, timeout: Optional[float]) -> None:
self._timeout_settings.set_default_timeout(timeout)
self._channel.send_no_reply(
"setDefaultTimeoutNoReply", {} if timeout is None else {"timeout": timeout}
)

@property
def pages(self) -> List[Page]:
Expand All @@ -310,14 +302,33 @@ def pages(self) -> List[Page]:
def browser(self) -> Optional["Browser"]:
return self._browser

def _set_options(self, context_options: Dict, browser_options: Dict) -> None:
self._options = context_options
if self._options.get("recordHar"):
self._har_recorders[""] = {
"path": self._options["recordHar"]["path"],
"content": self._options["recordHar"].get("content"),
}
self._tracing._traces_dir = browser_options.get("tracesDir")
async def _initialize_har_from_options(
self,
record_har_path: Optional[Union[Path, str]],
record_har_content: Optional[HarContentPolicy],
record_har_omit_content: Optional[bool],
record_har_url_filter: Optional[Union[Pattern[str], str]],
record_har_mode: Optional[HarMode],
) -> None:
if not record_har_path:
return
record_har_path = str(record_har_path)
if len(record_har_path) == 0:
return
# Forcibly provide type to satisfy mypy
default_policy: HarContentPolicy = (
"attach" if record_har_path.endswith(".zip") else "embed"
)
content_policy: HarContentPolicy = record_har_content or (
"omit" if record_har_omit_content is True else default_policy
)
await self._record_into_har(
har=record_har_path,
page=None,
url=record_har_url_filter,
update_content=content_policy,
update_mode=(record_har_mode or "full"),
)

async def new_page(self) -> Page:
if self._owner_page:
Expand Down Expand Up @@ -476,22 +487,25 @@ async def _record_into_har(
update_content: HarContentPolicy = None,
update_mode: HarMode = None,
) -> None:
update_content = update_content or "attach"
params: Dict[str, Any] = {
"options": prepare_record_har_options(
{
"recordHarPath": har,
"recordHarContent": update_content or "attach",
"recordHarMode": update_mode or "minimal",
"recordHarUrlFilter": url,
}
)
"options": {
"zip": str(har).endswith(".zip"),
"content": update_content,
"urlGlob": url if isinstance(url, str) else None,
"urlRegexSource": url.pattern if isinstance(url, Pattern) else None,
"urlRegexFlags": (
escape_regex_flags(url) if isinstance(url, Pattern) else None
),
"mode": update_mode or "minimal",
}
}
if page:
params["page"] = page._channel
har_id = await self._channel.send("harStart", params)
self._har_recorders[har_id] = {
"path": str(har),
"content": update_content or "attach",
"content": update_content,
}

async def route_from_har(
Expand Down Expand Up @@ -555,22 +569,30 @@ def expect_event(
return EventContextManagerImpl(waiter.result())

def _on_close(self) -> None:
self._closing_or_closed = True
if self._browser:
self._browser._contexts.remove(self)
try:
self._browser._contexts.remove(self)
except KeyError:
pass
try:
self._browser._browser_type._playwright.selectors._contextsForSelectors.remove(
self
)
except KeyError:
pass

self._dispose_har_routers()
self._tracing._reset_stack_counter()
self.emit(BrowserContext.Events.Close, self)

async def close(self, reason: str = None) -> None:
if self._close_was_called:
if self._closing_or_closed:
return
self._close_reason = reason
self._close_was_called = True
self._closing_or_closed = True

await self._channel._connection.wrap_api_call(
lambda: self.request.dispose(reason=reason), True
)
await self.request.dispose(reason=reason)

async def _inner_close() -> None:
for har_id, params in self._har_recorders.items():
Expand Down
Loading
Loading