diff --git a/AGENTS.md b/AGENTS.md index 0fa51a9a407d7..0f70128157e9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,7 @@ Guidance for AI agents working in the Selenium monorepo. Language-specific details live in respective subdirectories. --> See @.local/AGENTS.md for additional guidance +See [skills.md](skills.md) for a consolidated cross-language quickstart and skill matrix. Selenium is a Bazel-built monorepo implementing the W3C WebDriver (and related) protocols, shipping multiple language bindings plus Grid and Selenium Manager. diff --git a/py/selenium/webdriver/common/bidi/permissions.py b/py/selenium/webdriver/common/bidi/permissions.py index 17faa1ff5454f..6dd138da17309 100644 --- a/py/selenium/webdriver/common/bidi/permissions.py +++ b/py/selenium/webdriver/common/bidi/permissions.py @@ -15,12 +15,20 @@ # specific language governing permissions and limitations # under the License. +"""WebDriver BiDi Permissions module.""" -from selenium.webdriver.common.bidi.common import command_builder +from __future__ import annotations +from enum import Enum +from typing import Any -class PermissionState: - """Represents the possible permission states.""" +from .common import command_builder + +_VALID_PERMISSION_STATES = {"granted", "denied", "prompt"} + + +class PermissionState(str, Enum): + """Permission state enumeration.""" GRANTED = "granted" DENIED = "denied" @@ -28,56 +36,69 @@ class PermissionState: class PermissionDescriptor: - """Represents a permission descriptor.""" + """Descriptor for a permission.""" - def __init__(self, name: str): + def __init__(self, name: str) -> None: + """Initialize a PermissionDescriptor. + + Args: + name: The name of the permission (e.g., 'geolocation', 'microphone', 'camera') + """ self.name = name - def to_dict(self) -> dict: - return {"name": self.name} + def __repr__(self) -> str: + return f"PermissionDescriptor('{self.name}')" class Permissions: - """BiDi implementation of the permissions module.""" + """WebDriver BiDi Permissions module.""" - def __init__(self, conn): - self.conn = conn + def __init__(self, websocket_connection: Any) -> None: + """Initialize the Permissions module. + + Args: + websocket_connection: The WebSocket connection for sending BiDi commands + """ + self._conn = websocket_connection def set_permission( self, - descriptor: str | PermissionDescriptor, - state: str, - origin: str, + descriptor: PermissionDescriptor | str, + state: PermissionState | str, + origin: str | None = None, user_context: str | None = None, ) -> None: - """Sets a permission state for a given permission descriptor. + """Set a permission for a given origin. Args: - descriptor: The permission name (str) or PermissionDescriptor object. - Examples: "geolocation", "camera", "microphone". - state: The permission state (granted, denied, prompt). - origin: The origin for which the permission is set. - user_context: The user context id (optional). + descriptor: The permission descriptor or permission name as a string + state: The desired permission state + origin: The origin for which to set the permission + user_context: Optional user context ID to scope the permission Raises: - ValueError: If the permission state is invalid. + ValueError: If the state is not a valid permission state """ - if state not in [PermissionState.GRANTED, PermissionState.DENIED, PermissionState.PROMPT]: - valid_states = f"{PermissionState.GRANTED}, {PermissionState.DENIED}, {PermissionState.PROMPT}" - raise ValueError(f"Invalid permission state. Must be one of: {valid_states}") + state_value = state.value if isinstance(state, PermissionState) else state + if state_value not in _VALID_PERMISSION_STATES: + raise ValueError( + f"Invalid permission state: {state_value!r}. " + f"Must be one of {sorted(_VALID_PERMISSION_STATES)}" + ) if isinstance(descriptor, str): - permission_descriptor = PermissionDescriptor(descriptor) + descriptor_dict = {"name": descriptor} else: - permission_descriptor = descriptor + descriptor_dict = {"name": descriptor.name} - params = { - "descriptor": permission_descriptor.to_dict(), - "state": state, - "origin": origin, + params: dict[str, Any] = { + "descriptor": descriptor_dict, + "state": state_value, } - + if origin is not None: + params["origin"] = origin if user_context is not None: params["userContext"] = user_context - self.conn.execute(command_builder("permissions.setPermission", params)) + cmd = command_builder("permissions.setPermission", params) + self._conn.execute(cmd) diff --git a/py/selenium/webdriver/common/bidi/session.py b/py/selenium/webdriver/common/bidi/session.py index 3481c2d77842d..fcb42a4ad86fc 100644 --- a/py/selenium/webdriver/common/bidi/session.py +++ b/py/selenium/webdriver/common/bidi/session.py @@ -1,134 +1,246 @@ -# Licensed to the Software Freedom Conservancy (SFC) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The SFC licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at +# DO NOT EDIT THIS FILE! # -# http://www.apache.org/licenses/LICENSE-2.0 +# This file is generated from the WebDriver BiDi specification. If you need to make +# changes, edit the generator and regenerate all of the modules. # -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. +# WebDriver BiDi module: session +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Any -from selenium.webdriver.common.bidi.common import command_builder +from .common import command_builder class UserPromptHandlerType: - """Represents the behavior of the user prompt handler.""" + """UserPromptHandlerType.""" ACCEPT = "accept" DISMISS = "dismiss" IGNORE = "ignore" - VALID_TYPES = {ACCEPT, DISMISS, IGNORE} +@dataclass +class CapabilitiesRequest: + """CapabilitiesRequest.""" + always_match: Any | None = None + first_match: list[Any] = field(default_factory=list) + + +@dataclass +class CapabilityRequest: + """CapabilityRequest.""" + + accept_insecure_certs: bool | None = None + browser_name: str | None = None + browser_version: str | None = None + platform_name: str | None = None + proxy: Any | None = None + unhandled_prompt_behavior: Any | None = None + + +@dataclass +class AutodetectProxyConfiguration: + """AutodetectProxyConfiguration.""" + + proxy_type: str = field(default="autodetect", init=False) + + +@dataclass +class DirectProxyConfiguration: + """DirectProxyConfiguration.""" + + proxy_type: str = field(default="direct", init=False) + + +@dataclass +class ManualProxyConfiguration: + """ManualProxyConfiguration.""" + + proxy_type: str = field(default="manual", init=False) + http_proxy: str | None = None + ssl_proxy: str | None = None + no_proxy: list[Any] = field(default_factory=list) + + +@dataclass +class SocksProxyConfiguration: + """SocksProxyConfiguration.""" + + socks_proxy: str | None = None + socks_version: Any | None = None + + +@dataclass +class PacProxyConfiguration: + """PacProxyConfiguration.""" + + proxy_type: str = field(default="pac", init=False) + proxy_autoconfig_url: str | None = None + + +@dataclass +class SystemProxyConfiguration: + """SystemProxyConfiguration.""" + + proxy_type: str = field(default="system", init=False) + + +@dataclass +class SubscribeParameters: + """SubscribeParameters.""" + + events: list[str] = field(default_factory=list) + contexts: list[Any] = field(default_factory=list) + user_contexts: list[Any] = field(default_factory=list) + + +@dataclass +class UnsubscribeByIDRequest: + """UnsubscribeByIDRequest.""" + + subscriptions: list[Any] = field(default_factory=list) + + +@dataclass +class UnsubscribeByAttributesRequest: + """UnsubscribeByAttributesRequest.""" + + events: list[str] = field(default_factory=list) + + +@dataclass +class StatusResult: + """StatusResult.""" + + ready: bool | None = None + message: str | None = None + + +@dataclass +class NewParameters: + """NewParameters.""" + + capabilities: Any | None = None + + +@dataclass +class NewResult: + """NewResult.""" + + session_id: str | None = None + accept_insecure_certs: bool | None = None + browser_name: str | None = None + browser_version: str | None = None + platform_name: str | None = None + set_window_rect: bool | None = None + user_agent: str | None = None + proxy: Any | None = None + unhandled_prompt_behavior: Any | None = None + web_socket_url: str | None = None + + +@dataclass +class SubscribeResult: + """SubscribeResult.""" + + subscription: Any | None = None + + +@dataclass class UserPromptHandler: - """Represents the configuration of the user prompt handler.""" + """UserPromptHandler.""" - def __init__( - self, - alert: str | None = None, - before_unload: str | None = None, - confirm: str | None = None, - default: str | None = None, - file: str | None = None, - prompt: str | None = None, - ): - """Initialize UserPromptHandler. - - Args: - alert: Handler type for alert prompts. - before_unload: Handler type for beforeUnload prompts. - confirm: Handler type for confirm prompts. - default: Default handler type for all prompts. - file: Handler type for file picker prompts. - prompt: Handler type for prompt dialogs. - - Raises: - ValueError: If any handler type is not valid. - """ - for field_name, value in [ - ("alert", alert), - ("before_unload", before_unload), - ("confirm", confirm), - ("default", default), - ("file", file), - ("prompt", prompt), - ]: - if value is not None and value not in UserPromptHandlerType.VALID_TYPES: - raise ValueError( - f"Invalid {field_name} handler type: {value}. Must be one of {UserPromptHandlerType.VALID_TYPES}" - ) - - self.alert = alert - self.before_unload = before_unload - self.confirm = confirm - self.default = default - self.file = file - self.prompt = prompt - - def to_dict(self) -> dict[str, str]: - """Convert the UserPromptHandler to a dictionary for BiDi protocol. - - Returns: - Dictionary representation suitable for BiDi protocol. - """ - field_mapping = { - "alert": "alert", - "before_unload": "beforeUnload", - "confirm": "confirm", - "default": "default", - "file": "file", - "prompt": "prompt", - } + alert: Any | None = None + before_unload: Any | None = None + confirm: Any | None = None + default: Any | None = None + file: Any | None = None + prompt: Any | None = None + def to_bidi_dict(self) -> dict: + """Convert to BiDi protocol dict with camelCase keys.""" result = {} - for attr_name, dict_key in field_mapping.items(): - value = getattr(self, attr_name) - if value is not None: - result[dict_key] = value + if self.alert is not None: + result["alert"] = self.alert + if self.before_unload is not None: + result["beforeUnload"] = self.before_unload + if self.confirm is not None: + result["confirm"] = self.confirm + if self.default is not None: + result["default"] = self.default + if self.file is not None: + result["file"] = self.file + if self.prompt is not None: + result["prompt"] = self.prompt return result - class Session: - def __init__(self, conn): - self.conn = conn + """WebDriver BiDi session module.""" - def subscribe(self, *events, browsing_contexts=None): + def __init__(self, conn) -> None: + self._conn = conn + + def status(self): + """Execute session.status.""" params = { - "events": events, } - if browsing_contexts is None: - browsing_contexts = [] - if browsing_contexts: - params["browsingContexts"] = browsing_contexts - return command_builder("session.subscribe", params) + params = {k: v for k, v in params.items() if v is not None} + cmd = command_builder("session.status", params) + result = self._conn.execute(cmd) + return result + + def new(self, capabilities: Any | None = None): + """Execute session.new.""" + if capabilities is None: + raise TypeError("new() missing required argument: {{snake_param!r}}") - def unsubscribe(self, *events, browsing_contexts=None): params = { - "events": events, + "capabilities": capabilities, } - if browsing_contexts is None: - browsing_contexts = [] - if browsing_contexts: - params["browsingContexts"] = browsing_contexts - return command_builder("session.unsubscribe", params) + params = {k: v for k, v in params.items() if v is not None} + cmd = command_builder("session.new", params) + result = self._conn.execute(cmd) + return result - def status(self): - """The session.status command returns information about the remote end's readiness. + def end(self): + """Execute session.end.""" + params = { + } + params = {k: v for k, v in params.items() if v is not None} + cmd = command_builder("session.end", params) + result = self._conn.execute(cmd) + return result - Returns information about the remote end's readiness to create new sessions - and may include implementation-specific metadata. + def subscribe( + self, + events: list[Any] | None = None, + contexts: list[Any] | None = None, + user_contexts: list[Any] | None = None, + ): + """Execute session.subscribe.""" + if events is None: + raise TypeError("subscribe() missing required argument: {{snake_param!r}}") + + params = { + "events": events, + "contexts": contexts, + "userContexts": user_contexts, + } + params = {k: v for k, v in params.items() if v is not None} + cmd = command_builder("session.subscribe", params) + result = self._conn.execute(cmd) + return result + + def unsubscribe(self, events: list[Any] | None = None, subscriptions: list[Any] | None = None): + """Execute session.unsubscribe.""" + params = { + "events": events, + "subscriptions": subscriptions, + } + params = {k: v for k, v in params.items() if v is not None} + cmd = command_builder("session.unsubscribe", params) + result = self._conn.execute(cmd) + return result - Returns: - Dictionary containing the ready state (bool), message (str) and metadata. - """ - cmd = command_builder("session.status", {}) - return self.conn.execute(cmd) diff --git a/skills.md b/skills.md new file mode 100644 index 0000000000000..9be642c035244 --- /dev/null +++ b/skills.md @@ -0,0 +1,217 @@ +# Selenium Repository Skills Guide + +This document captures best practices for contributors and coding agents working in the Selenium monorepo. + +See also: [AGENTS.md](AGENTS.md) and language-specific guides such as `/AGENTS.md` for per-language agent usage details. Those files are the authoritative source of truth; this guide is a consolidated companion. + +## Purpose + +Use this guide to make safe, focused changes across Selenium's multi-language bindings while staying aligned with Bazel-based workflows and project invariants. + +## Core Principles + +- Preserve API and ABI compatibility unless explicitly asked to break it. +- Prefer small, reversible diffs over broad refactors. +- Avoid repo-wide formatting changes. +- Treat `third_party/` as read-only and `bazel-*/` as generated output. +- Prefer targeted Bazel commands and discover labels with `bazel query` before build and test. +- For user-visible behavior changes, compare with at least one other binding (Java, Python, Ruby, .NET, JavaScript). +- If behavior is shared (protocol, remote transport, serialization), call out parity follow-up work. + +## High-Risk Areas + +Request explicit verification before making substantial changes in: + +- `common/` and `common/src/` +- `javascript/atoms/` +- `rust/` (Selenium Manager) +- `scripts/`, `rake_tasks/`, `.github/`, and `Rakefile` +- WebDriver and BiDi wire semantics, capability parsing, or Grid routing/distributor logic +- Build dependency wiring (`MODULE.bazel`, repin flows) + +## Universal Bazel Workflow + +1. Find likely targets with query: + +```bash +bazel query 'kind(".* rule", //path/to/area/...)' +``` + +2. Build only affected targets: + +```bash +bazel build //path/to/area:target +``` + +3. Run the smallest meaningful tests first: + +```bash +bazel test //path/to/area:target --test_output=all +``` + +4. Expand test scope only when the focused tests pass. + +### Useful Test Flags + +- `--test_size_filters=small` for fast unit-style tests +- `--test_output=all` to show test output +- `--cache_test_results=no` to force a rerun during debugging + +### Sandbox Notes + +If output directory permissions are restricted, use: + +```bash +bazel --output_base=.local/bazel-out +``` + +## Skill Matrix By Language + +Use each language section as a practical checklist before opening a PR. + +### Python Skill + +- Code location: `py/selenium/` +- Build: + +```bash +bazel build //py/... +``` + +- Tests: + +```bash +bazel test //py/... +``` + +- Browser-focused patterns: + +```bash +bazel test //py:test-chrome +bazel test //py:test-firefox-bidi +``` + +- Type hints: prefer union syntax (`str | None`) over `Optional[str]`. +- Runtime baseline: Python 3.10+. +- Logging: use `logging.getLogger(__name__)` and leveled messages. +- Deprecation: use `warnings.warn(..., DeprecationWarning, stacklevel=2)`. +- Public docs: use Google-style docstrings. + +### Ruby Skill + +- Code location: `rb/lib/selenium/webdriver` +- Tests: `rb/spec/unit/`, `rb/spec/integration/` +- Type signatures: `rb/sig/` +- Build: + +```bash +bazel build //rb/... +``` + +- Test discovery: + +```bash +bazel query 'kind(".*(test|suite) rule", //rb/...)' +``` + +- Logging: use `WebDriver.logger` with warning/info/debug levels. +- Deprecation: use `WebDriver.logger.deprecate(...)` with an explicit replacement. +- Internal APIs: mark with `@api private` in YARD comments. +- Public API changes: update corresponding `.rbs` files. +- Public docs: use YARD annotations. + +### .NET (C#) Skill + +- Core code: `dotnet/src/webdriver/` +- Support code: `dotnet/src/support/` +- Tests: `dotnet/test/common/` +- Build: + +```bash +bazel build //dotnet/... +``` + +- Test discovery: + +```bash +bazel query 'kind(".*(test|suite) rule", //dotnet/...)' +``` + +- Logging: use `OpenQA.Selenium.Internal.Logging` and `Log.GetLogger()`. +- Deprecation: use `[Obsolete("Use NewMethod instead")]`. +- Async direction: prefer async-compatible changes when adding new APIs. +- Public docs: use XML documentation comments. + +### Java Skill + +- Java bindings: `java/src/`, `java/test/` +- Grid code: `java/src/org/openqa/selenium/grid/` +- Build: + +```bash +bazel build //java/... +bazel build grid +``` + +- Test discovery: + +```bash +bazel query 'kind(".*(test|suite) rule", //java/...)' +``` + +- Logging: use `java.util.logging.Logger` with warning/info/fine. +- Deprecation: use `@Deprecated(forRemoval = true)` with migration path. +- Public docs: use Javadoc for public APIs. + +### JavaScript Skill + +- Library: `javascript/selenium-webdriver/lib/` +- Tests: `javascript/selenium-webdriver/test/` +- Build: + +```bash +bazel build //javascript/selenium-webdriver/... +``` + +- Test discovery: + +```bash +bazel query 'kind(".*(test|suite) rule", //javascript/selenium-webdriver/...)' +``` + +- Logging: use module logger via `logging.getLogger(...)`. +- Deprecation: emit warning with clear replacement path. +- Public docs: use JSDoc. + +## Cross-Binding Consistency Checklist + +Before merging user-visible behavior changes: + +1. Search comparable behavior in at least one other binding. + +```bash +rg '' java/ py/ rb/ dotnet/ javascript/selenium-webdriver/ +``` + +2. Record whether behavior is intentionally aligned or intentionally divergent. +3. If divergent, include a follow-up issue or rationale in the PR description. + +## Pre-PR Checklist + +- Target labels discovered with `bazel query`. +- Changed targets build successfully. +- Focused tests pass with `--test_output=all` where useful. +- Cross-binding review completed for user-visible behavior. +- High-risk changes called out clearly. +- Formatting and linting run via repo tooling when applicable (`./scripts/format.sh` or `./go all:lint`). + +## Ownership And Maintenance + +Update this guide when any of the following changes: + +- Language layout paths +- Bazel target conventions +- Logging, deprecation, or public documentation conventions +- High-risk area definitions + +The language-specific AGENTS files and the root `AGENTS.md` are the authoritative source of truth. This guide is a consolidated summary and must not contradict them. When a language-specific AGENTS file changes, update the corresponding section here to stay consistent.