Skip to content

Commit c6e340b

Browse files
committed
feat(electron): add Electron application support
Add three new keywords: - New Electron Application: launch Electron app via playwright._electron - Close Electron Application: close active Electron app + remove state - Open Electron Dev Tools: open DevTools in Electron browser window Includes: - gRPC service bindings (launchElectron, closeElectron, openElectronDevTools) - Minimal Electron test app (node/electron-test-app/) - Acceptance tests (atest/test/01_Browser_Management/electron.robot) - Python keyword class (Browser/keywords/electron.py) - Proto definitions for new RPCs - ESLint/prettier compliant (rebased onto ms_main e32f760+92f4ea5) https://claude.ai/code/session_011ivCRcjRk93AMkjLwsbquz
1 parent 92f4ea5 commit c6e340b

15 files changed

Lines changed: 1615 additions & 3 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ results
6060

6161
# linting
6262
.ruff_cache
63+
64+
# Chromium IPC pipe socket created by the Electron process at runtime
65+
node/electron-test-app/PIPE

Browser/browser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
Cookie,
4343
Coverage,
4444
Devices,
45+
Electron,
4546
Evaluation,
4647
Formatter,
4748
Getters,
@@ -854,6 +855,7 @@ def __init__( # noqa: PLR0915
854855
Coverage(self),
855856
Crawling(self),
856857
Devices(self),
858+
Electron(self),
857859
Evaluation(self),
858860
self._assertion_formatter,
859861
Interaction(self),

Browser/keywords/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .cookie import Cookie
1919
from .coverage import Coverage
2020
from .device_descriptors import Devices
21+
from .electron import Electron
2122
from .evaluation import Evaluation
2223
from .getters import Getters
2324
from .interaction import Interaction
@@ -37,6 +38,7 @@
3738
"Cookie",
3839
"Coverage",
3940
"Devices",
41+
"Electron",
4042
"Evaluation",
4143
"Formatter",
4244
"Getters",

Browser/keywords/electron.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Copyright 2020- Robot Framework Foundation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
from copy import copy
17+
from datetime import timedelta
18+
from pathlib import Path
19+
20+
from ..base import LibraryComponent
21+
from ..generated.playwright_pb2 import Request
22+
from ..utils import (
23+
ColorScheme,
24+
GeoLocation,
25+
HttpCredentials,
26+
NewPageDetails,
27+
RecordHar,
28+
RecordVideo,
29+
ViewportDimensions,
30+
keyword,
31+
locals_to_params,
32+
logger,
33+
)
34+
35+
36+
class Electron(LibraryComponent):
37+
"""Keywords for launching and controlling Electron applications."""
38+
39+
@keyword(tags=("Setter", "BrowserControl"))
40+
def new_electron_application(
41+
self,
42+
executable_path: Path,
43+
args: list[str] | None = None,
44+
env: dict[str, str] | None = None,
45+
timeout: timedelta = timedelta(seconds=30),
46+
*,
47+
acceptDownloads: bool = True,
48+
bypassCSP: bool = False,
49+
colorScheme: ColorScheme | None = None,
50+
cwd: str | None = None,
51+
extraHTTPHeaders: dict[str, str] | None = None,
52+
geolocation: GeoLocation | None = None,
53+
hasTouch: bool | None = None,
54+
httpCredentials: HttpCredentials | None = None,
55+
ignoreHTTPSErrors: bool = False,
56+
isMobile: bool | None = None,
57+
javaScriptEnabled: bool = True,
58+
locale: str | None = None,
59+
offline: bool = False,
60+
recordHar: RecordHar | None = None,
61+
recordVideo: RecordVideo | None = None,
62+
slowMo: timedelta = timedelta(seconds=0),
63+
timezoneId: str | None = None,
64+
tracesDir: str | None = None,
65+
viewport: ViewportDimensions | None = ViewportDimensions(
66+
width=1280, height=720
67+
),
68+
) -> tuple[str, str, NewPageDetails]:
69+
"""Launches an Electron application and sets its first window as the active page.
70+
71+
Uses Playwright's ``_electron.launch()`` API to start the application, then
72+
attaches the first window as the active ``Page``. All standard Browser library
73+
page keywords (``Click``, ``Get Text``, ``Wait For Elements State``, …) work
74+
against the Electron window without any extra setup.
75+
76+
Returns a ``(browser_id, context_id, page_details)`` tuple — the same shape as
77+
`New Persistent Context` — so ``Switch Page`` and friends work if multiple
78+
windows are open.
79+
80+
*Note on headless mode*
81+
82+
Playwright does not expose a ``headless`` option for Electron — the application
83+
always opens a native GUI window. On Linux CI machines, run the process inside a
84+
virtual display:
85+
86+
| # Before running tests, start a virtual display:
87+
| Xvfb :99 -screen 0 1280x720x24 &
88+
| export DISPLAY=:99
89+
90+
| =Argument= | =Description= |
91+
| ``executable_path`` | Path to the Electron binary or packaged application executable. When using the bare ``electron`` npm package pass the path to ``main.js`` via ``args``. |
92+
| ``args`` | Additional command-line arguments forwarded to the Electron process. Use this to pass the entry-point script when running the bare Electron binary, e.g. ``[node/my-app/main.js]``. |
93+
| ``env`` | Environment variables for the launched process. Merged on top of the current process environment so ``PATH`` and other system variables are still inherited. |
94+
| ``timeout`` | Maximum time to wait for the first window to appear. Defaults to ``30 seconds``. Pass ``0`` to disable. |
95+
| ``acceptDownloads`` | Whether to automatically download all attachments. Defaults to ``True``. |
96+
| ``bypassCSP`` | Toggles bypassing page's Content-Security-Policy. Defaults to ``False``. |
97+
| ``colorScheme`` | Emulates ``prefers-color-scheme`` media feature: ``dark``, ``light``, ``no-preference``, or ``null`` to disable emulation. |
98+
| ``cwd`` | Working directory for the launched Electron process. Defaults to the current working directory. |
99+
| ``extraHTTPHeaders`` | Additional HTTP headers sent with every renderer request. |
100+
| ``geolocation`` | Geolocation to emulate. Dictionary with ``latitude``, ``longitude``, and optional ``accuracy`` keys. |
101+
| ``hasTouch`` | Whether the viewport should support touch events. |
102+
| ``httpCredentials`` | Credentials for HTTP Basic Authentication. Dictionary with ``username`` and ``password`` keys. |
103+
| ``ignoreHTTPSErrors`` | Whether to ignore HTTPS errors during navigation. Defaults to ``False``. |
104+
| ``isMobile`` | Whether to emulate a mobile device (meta-viewport tag, touch events, …). |
105+
| ``javaScriptEnabled`` | Whether to enable JavaScript in the renderer. Defaults to ``True``. |
106+
| ``locale`` | Renderer locale, e.g. ``en-GB``. Affects ``navigator.language`` and date/number formatting. |
107+
| ``offline`` | Emulates network being offline. Defaults to ``False``. |
108+
| ``recordHar`` | Enable HAR recording. Dictionary with ``path`` (required) and optional ``omitContent``. |
109+
| ``recordVideo`` | Enable video recording. Dictionary with ``dir`` (required) and optional ``size``. |
110+
| ``slowMo`` | Slows down all Playwright operations by the given duration. Useful for visual debugging. Defaults to no delay. |
111+
| ``timezoneId`` | Overrides the system timezone for the renderer, e.g. ``Europe/Berlin``. |
112+
| ``tracesDir`` | Directory where Playwright trace files are written. |
113+
| ``viewport`` | Initial viewport dimensions. Defaults to ``{'width': 1280, 'height': 720}``. Pass ``None`` to use the native window size. |
114+
115+
Example — bare Electron binary with a source checkout:
116+
| ${ELECTRON}= Set Variable node_modules/.bin/electron
117+
| @{ARGS}= Create List node/electron-test-app/main.js
118+
| ${browser} ${context} ${page}= `New Electron Application`
119+
| ... executable_path=${ELECTRON} args=@{ARGS}
120+
| `Get Title` == My App
121+
122+
Example — packaged application executable:
123+
| ${browser} ${context} ${page}= `New Electron Application`
124+
| ... executable_path=/usr/share/myapp/myapp
125+
| `Get Title` == My App
126+
127+
Example — French locale, larger viewport, video recording:
128+
| &{VIDEO}= Create Dictionary dir=videos
129+
| ${browser} ${context} ${page}= `New Electron Application`
130+
| ... executable_path=/usr/share/myapp/myapp
131+
| ... locale=fr-FR
132+
| ... viewport={'width': 1920, 'height': 1080}
133+
| ... recordVideo=${VIDEO}
134+
135+
[https://forum.robotframework.org/t//4309|Comment >>]
136+
"""
137+
options = locals_to_params(locals())
138+
timeout_ms = int(timeout.total_seconds() * 1000)
139+
slow_mo_ms = int(slowMo.total_seconds() * 1000)
140+
141+
options["executablePath"] = str(options.pop("executable_path"))
142+
options["timeout"] = timeout_ms
143+
options.pop("slowMo", None)
144+
if slow_mo_ms > 0:
145+
options["slowMo"] = slow_mo_ms
146+
if viewport is not None:
147+
options["viewport"] = copy(viewport)
148+
149+
with self.playwright.grpc_channel() as stub:
150+
response = stub.LaunchElectron(
151+
Request().ElectronLaunch(
152+
rawOptions=json.dumps(options, default=str),
153+
defaultTimeout=timeout_ms,
154+
)
155+
)
156+
logger.info(response.log)
157+
158+
if recordVideo is not None:
159+
self.context_cache.add(response.id, self.library._playwright_state._get_video_size(options))
160+
video_path = self.library._playwright_state._embed_video(json.loads(response.video))
161+
162+
return (
163+
response.browserId,
164+
response.id,
165+
NewPageDetails(page_id=response.pageId, video_path=video_path),
166+
)
167+
168+
@keyword(tags=("Setter", "BrowserControl"))
169+
def close_electron_application(self) -> None:
170+
"""Closes the running Electron application and cleans up library state.
171+
172+
Equivalent to `Close Browser` for Electron apps. Closes the
173+
``ElectronApplication`` handle and removes the associated browser, context,
174+
and page from the Browser library state stack.
175+
176+
After this keyword there is no active browser; call `New Electron Application`,
177+
`New Browser`, or `New Persistent Context` before issuing further page
178+
interactions.
179+
180+
Calling this keyword when no Electron app is open is safe — it logs a message
181+
and does nothing.
182+
183+
Example:
184+
| `New Electron Application` executable_path=/path/to/app
185+
| # ... test steps ...
186+
| [Teardown] `Close Electron Application`
187+
188+
[https://forum.robotframework.org/t//4309|Comment >>]
189+
"""
190+
with self.playwright.grpc_channel() as stub:
191+
response = stub.CloseElectron(Request().Empty())
192+
logger.info(response.log)
193+
194+
@keyword(tags=("Getter", "BrowserControl"))
195+
def open_electron_dev_tools(self) -> None:
196+
"""Opens Chromium DevTools for every window of the running Electron application.
197+
198+
Calls ``BrowserWindow.getAllWindows()`` in the Electron **main process** via
199+
``ElectronApplication.evaluate()``. Node.js and Electron APIs are only
200+
available in the main process — renderer contexts (where `Evaluate JavaScript`
201+
runs) cannot access them.
202+
203+
Intended as a development-time debugging aid: use it to inspect the live DOM,
204+
find element selectors, and debug JavaScript.
205+
206+
Example:
207+
| `New Electron Application` executable_path=/path/to/app
208+
| `Open Electron Dev Tools`
209+
| Sleep 30s # manually inspect the DevTools panel
210+
| `Close Electron Application`
211+
212+
[https://forum.robotframework.org/t//4309|Comment >>]
213+
"""
214+
with self.playwright.grpc_channel() as stub:
215+
response = stub.OpenElectronDevTools(Request().Empty())
216+
logger.info(response.log)

atest/library/electron_setup.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import sys
2+
from pathlib import Path
3+
4+
from robot.api.exceptions import SkipExecution
5+
6+
7+
def get_electron_binary_path(app_dir: str) -> str:
8+
"""Return the platform-specific Electron binary path for the test suite.
9+
10+
Raises SkipExecution when app_dir does not exist so the caller suite is
11+
gracefully skipped (e.g. BrowserBatteries runs that remove node/).
12+
13+
The Electron binary is resolved from the project root node_modules/,
14+
which is populated by npm ci at project level before tests run.
15+
"""
16+
app_path = Path(app_dir)
17+
if not app_path.exists():
18+
raise SkipExecution(
19+
f"Electron test app not present ({app_dir} missing) — skipping suite."
20+
)
21+
22+
electron_dist = app_path.parent.parent / "node_modules" / "electron" / "dist"
23+
24+
if sys.platform == "win32":
25+
return str(electron_dist / "electron.exe")
26+
if sys.platform == "darwin":
27+
return str(electron_dist / "Electron.app" / "Contents" / "MacOS" / "Electron")
28+
return str(electron_dist / "electron")

0 commit comments

Comments
 (0)