Skip to content

Commit a4f85d0

Browse files
committed
Added support with video in selenium
1 parent 928af5a commit a4f85d0

File tree

4 files changed

+123
-8
lines changed

4 files changed

+123
-8
lines changed

core/testcontainers/core/container.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from docker.models.containers import Container
21
import os
32
from typing import Iterable, Optional, Tuple
43

5-
from .waiting_utils import wait_container_is_ready
4+
from docker.models.containers import Container
5+
66
from .docker_client import DockerClient
77
from .exceptions import ContainerStartException
88
from .utils import setup_logger, inside_container, is_arm
9+
from .waiting_utils import wait_container_is_ready
910

1011
logger = setup_logger(__name__)
1112

@@ -28,11 +29,16 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs
2829
self.volumes = {}
2930
self.image = image
3031
self._docker = DockerClient(**(docker_client_kw or {}))
32+
self.network_name = None
3133
self._container = None
3234
self._command = None
3335
self._name = None
3436
self._kwargs = kwargs
3537

38+
@property
39+
def name(self):
40+
return self._container.name
41+
3642
def with_env(self, key: str, value: str) -> 'DockerContainer':
3743
self.env[key] = value
3844
return self
@@ -55,12 +61,16 @@ def maybe_emulate_amd64(self) -> 'DockerContainer':
5561
return self.with_kwargs(platform='linux/amd64')
5662
return self
5763

64+
def set_network_name(self, network_name: str) -> 'DockerContainer':
65+
self.network_name = network_name
66+
return self
67+
5868
def start(self) -> 'DockerContainer':
5969
logger.info("Pulling image %s", self.image)
6070
docker_client = self.get_docker_client()
6171
self._container = docker_client.run(
6272
self.image, command=self._command, detach=True, environment=self.env, ports=self.ports,
63-
name=self._name, volumes=self.volumes, **self._kwargs
73+
name=self._name, volumes=self.volumes, network=self.network_name, **self._kwargs
6474
)
6575
logger.info("Container started: %s", self._container.short_id)
6676
return self

selenium/testcontainers/selenium/__init__.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,22 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13+
import uuid
14+
from pathlib import Path
15+
from typing import Optional
1316

17+
import urllib3
1418
from selenium import webdriver
1519
from testcontainers.core.container import DockerContainer
1620
from testcontainers.core.waiting_utils import wait_container_is_ready
17-
from typing import Optional
18-
import urllib3
1921

22+
from .video import SeleniumVideoContainer
23+
24+
EMPTY_PATH = "."
2025

2126
IMAGES = {
22-
"firefox": "selenium/standalone-firefox-debug:latest",
23-
"chrome": "selenium/standalone-chrome-debug:latest"
27+
"firefox": "selenium/standalone-firefox:4.13.0-20231004",
28+
"chrome": "selenium/standalone-chrome:4.13.0-20231004"
2429
}
2530

2631

@@ -53,6 +58,8 @@ def __init__(self, capabilities: str, image: Optional[str] = None, port: int = 4
5358
self.vnc_port = vnc_port
5459
super(BrowserWebDriverContainer, self).__init__(image=self.image, **kwargs)
5560
self.with_exposed_ports(self.port, self.vnc_port)
61+
self.video = None
62+
self.network = None
5663

5764
def _configure(self) -> None:
5865
self.with_env("no_proxy", "localhost")
@@ -71,3 +78,43 @@ def get_connection_url(self) -> str:
7178
ip = self.get_container_host_ip()
7279
port = self.get_exposed_port(self.port)
7380
return f'http://{ip}:{port}/wd/hub'
81+
82+
def with_video(self, video_path: Path = Path.cwd()) -> 'DockerContainer':
83+
self.video = SeleniumVideoContainer()
84+
85+
target_video_path = video_path.parent
86+
if target_video_path.samefile(EMPTY_PATH):
87+
target_video_path = Path.cwd()
88+
self.video.save_videos(str(target_video_path))
89+
90+
if video_path.name:
91+
self.video.set_video_name(video_path.name)
92+
93+
return self
94+
95+
def start(self) -> 'DockerContainer':
96+
if self.video:
97+
self.network = self._docker.client.networks.create(str(uuid.uuid1()))
98+
self.set_network_name(self.network.name)
99+
self.video.set_network_name(self.network.name)
100+
101+
super().start()
102+
103+
self.video.set_selenium_container_host(self.get_wrapped_container().short_id)
104+
self.video.start()
105+
106+
return self
107+
108+
super().start()
109+
return self
110+
111+
def stop(self, force=True, delete_volume=True) -> None:
112+
if self.video:
113+
# Video need to stop before remove
114+
self.video.get_wrapped_container().stop()
115+
self.video.stop(force, delete_volume)
116+
117+
super().stop(force, delete_volume)
118+
119+
if self.network:
120+
self.get_docker_client().client.api.remove_network(self.network.id)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain
4+
# a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
# License for the specific language governing permissions and limitations
12+
# under the License.
13+
from typing import Optional
14+
15+
from testcontainers.core.container import DockerContainer
16+
17+
VIDEO_DEFAULT_IMAGE = "selenium/video:ffmpeg-4.3.1-20231004"
18+
19+
20+
class SeleniumVideoContainer(DockerContainer):
21+
"""
22+
Selenium video container.
23+
"""
24+
25+
def __init__(self, image: Optional[str] = None, **kwargs) -> None:
26+
self.image = image or VIDEO_DEFAULT_IMAGE
27+
super().__init__(image=self.image, **kwargs)
28+
29+
def set_video_name(self, video_name: str) -> 'DockerContainer':
30+
self.with_env("FILE_NAME", video_name)
31+
return self
32+
33+
def save_videos(self, host_path: str) -> 'DockerContainer':
34+
self.with_volume_mapping(host_path, "/videos", "rw")
35+
return self
36+
37+
def set_selenium_container_host(self, host: str) -> 'DockerContainer':
38+
self.with_env("DISPLAY_CONTAINER_NAME", host)
39+
return self

selenium/tests/test_selenium.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import tempfile
2+
from pathlib import Path
3+
14
import pytest
25
from selenium.webdriver import DesiredCapabilities
3-
from testcontainers.selenium import BrowserWebDriverContainer
46
from testcontainers.core.utils import is_arm
7+
from testcontainers.selenium import BrowserWebDriverContainer
58

69

710
@pytest.mark.parametrize("caps", [DesiredCapabilities.CHROME, DesiredCapabilities.FIREFOX])
@@ -20,3 +23,19 @@ def test_selenium_custom_image():
2023
chrome = BrowserWebDriverContainer(DesiredCapabilities.CHROME, image=image)
2124
assert "image" in dir(chrome), "`image` attribute was not instantialized."
2225
assert chrome.image == image, "`image` attribute was not set to the user provided value"
26+
27+
28+
@pytest.fixture
29+
def workdir() -> Path:
30+
tmpdir = tempfile.TemporaryDirectory()
31+
yield Path(tmpdir.name)
32+
tmpdir.cleanup()
33+
34+
35+
@pytest.mark.parametrize("caps", [DesiredCapabilities.CHROME])
36+
def test_selenium_video(caps, workdir):
37+
video_path = workdir / Path("video.mp4")
38+
with BrowserWebDriverContainer(caps).with_video(video_path) as chrome:
39+
chrome.get_driver()
40+
41+
assert video_path.exists(), "Selenium video file does not exists"

0 commit comments

Comments
 (0)