[UPD] script selenium better tools to navigate into Odoo

- support external selenium service to support selenium from Odoo
instance
- move selenium installation to erplibre virtual environment
- support user test instead of admin
This commit is contained in:
Mathieu Benoit 2025-10-31 01:12:25 -04:00
parent cbc43fde3c
commit 3d6b340b6d
3 changed files with 691 additions and 129 deletions

View file

@ -3,19 +3,22 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import datetime
import getpass
import json
import logging
import os
import random
import re
import shutil
import subprocess
import sys
import time
import json
import tempfile
from subprocess import getoutput
import time
import tkinter as tk
from subprocess import getoutput
from tkinter import filedialog
import getpass
from pykeepass import PyKeePass
from pykeepass import PyKeePass
from randomwordfr import RandomWordFr
from selenium import webdriver
from selenium.common.exceptions import (
@ -30,17 +33,54 @@ from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.ui import Select, WebDriverWait
from script.config import config_file
logging.basicConfig(
format=(
"%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]"
" %(message)s"
),
datefmt="%Y-%m-%d:%H:%M:%S",
level=logging.INFO,
)
_logger = logging.getLogger(__name__)
# TODO maybe use TODO lib
CONFIG_FILE = "./script/todo/todo.json"
CONFIG_OVERRIDE_FILE = "./private/todo.json"
CONFIG_OVERRIDE_FILE = "./private/todo/todo.json"
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
MONTHS_FR = {
"janvier": 1,
"février": 2,
"fevrier": 2,
"mars": 3,
"avril": 4,
"mai": 5,
"juin": 6,
"juillet": 7,
"août": 8,
"aout": 8,
"septembre": 9,
"octobre": 10,
"novembre": 11,
"décembre": 12,
"decembre": 12,
}
# TODO maybe use TODO lib
CONFIG_FILE = "./script/todo/todo.json"
CONFIG_OVERRIDE_FILE = "./private/todo/todo.json"
LOGO_ASCII_FILE = "./script/todo/logo_ascii.txt"
class SeleniumLib(object):
def __init__(self, config):
self.config = config
self.wait_human_time = config.human_time_speed
self.config_file = config_file.ConfigFile()
self.kdbx = None
self.video_recorder = None
self.default_download_dir_path = tempfile.mkdtemp()
@ -60,8 +100,13 @@ class SeleniumLib(object):
)
self.dirname_recording = dir_path_screencast
self.driver = None
with open(".odoo-version") as txt:
self.odoo_version = txt.read()
if os.path.isfile(".odoo-version"):
with open(".odoo-version") as txt:
self.odoo_version = txt.read()
else:
# Default version
# TODO instead, need to detect the version from client and detection
self.odoo_version = "18.0"
def do_screenshot(self):
if self.config.scenario_screenshot:
@ -113,25 +158,155 @@ class SeleniumLib(object):
# Créez une instance du navigateur Firefox avec les options de navigation privée
try:
self.driver = webdriver.Firefox(
options=firefox_options, service=firefox_services
user = os.environ.get("USER", "Non défini")
uid = os.getuid()
gid = os.getgid()
print(f"INFO: Execute by user: {user}, UID: {uid}, GID: {gid}")
# Exécuter la commande 'whoami' pour confirmation
whoami_result = subprocess.run(
["whoami"], capture_output=True, text=True
)
except Exception:
print(f"INFO: Result 'whoami': {whoami_result.stdout.strip()}")
except Exception as e:
print(f"ERROR when check user: {e}")
try:
if self.config.use_chrome_driver:
from selenium.webdriver.chrome.options import (
Options as ChromeOptions,
)
from selenium.webdriver.chrome.service import Service
chrome_options = ChromeOptions()
if self.config.window_size:
# chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument(
f"--window-size={self.config.window_size}"
)
if self.config.use_network:
chrome_options.set_capability(
"se:name", "ERPLibre selenium"
)
self.driver = webdriver.Remote(
options=chrome_options,
command_executor=self.config.use_network,
)
else:
from webdriver_manager.chrome import ChromeDriverManager
service = Service(
ChromeDriverManager().install(),
log_path="/tmp/chromedriver.log",
)
self.driver = webdriver.Chrome(
options=chrome_options,
# options=firefox_options,
service=service,
keep_alive=True,
)
elif self.config.use_firefox_driver:
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
firefox_services = None
# shutil.which("geckodriver")
if self.config.gecko_binary_path:
firefox_services = Service(
executable_path=self.config.gecko_binary_path,
log_output=self.config.debug,
)
firefox_options = webdriver.FirefoxOptions()
if not self.config.not_private_mode:
firefox_options.add_argument("--private")
if self.config.firefox_binary_path:
firefox_options.binary_location = (
self.config.firefox_binary_path
)
elif not self.config.use_network:
status_location = subprocess.check_output(
["which", "firefox"], text=True
).strip()
firefox_options.binary_location = status_location
firefox_profile = webdriver.FirefoxProfile()
firefox_profile.set_preference(
"browser.cache.disk.enable", False
)
firefox_profile.set_preference(
"browser.cache.memory.enable", False
)
firefox_profile.set_preference(
"browser.cache.offline.enable", False
)
firefox_profile.set_preference("network.http.use-cache", False)
firefox_options.set_preference(
"permissions.default.desktop-notification", 1
)
firefox_options.set_preference(
"browser.download.folderList", 2
)
firefox_options.set_preference(
"browser.download.manager.showWhenStarting", False
)
firefox_options.set_preference(
"browser.download.dir", self.default_download_dir_path
)
firefox_options.set_preference(
"browser.helperApps.neverAsk.saveToDisk",
"application/octet-stream,application/pdf,application/x-pdf",
)
firefox_options.set_preference("pdfjs.disabled", True)
if self.config.window_size:
# chrome_options.add_argument("--window-size=1920,1080")
firefox_options.add_argument(
f"--window-size={self.config.window_size}"
)
if self.config.headless:
firefox_options.add_argument("--headless")
# firefox_options.add_argument("--disable-gpu")
firefox_options.add_argument("--no-sandbox")
firefox_options.add_argument("--disable-dev-shm-usage")
firefox_options.add_argument("--disable-dbus")
if self.config.use_network:
firefox_options.set_capability(
"se:name", "ERPLibre selenium"
)
self.driver = webdriver.Remote(
options=firefox_options,
command_executor=self.config.use_network,
)
else:
self.driver = webdriver.Firefox(
options=firefox_options,
service=firefox_services,
keep_alive=True,
)
except Exception as e:
print(
"Cannot open Firefox profile, so will force firefox snap for"
" Ubuntu users."
)
firefox_services = Service(
executable_path=getoutput(
"find /snap/firefox -name geckodriver"
).split("\n")[-1]
)
firefox_options.binary_location = getoutput(
"find /snap/firefox -name firefox"
).split("\n")[-1]
self.driver = webdriver.Firefox(
options=firefox_options, service=firefox_services
)
# TODO au lieu, faire une recherche de geckodriver avant de démarrer
# firefox_services = Service(
# executable_path=getoutput(
# "find /snap/firefox -name geckodriver"
# ).split("\n")[-1], log_path='/tmp/geckodriver.log'
# )
# firefox_options.binary_location = getoutput(
# "find /snap/firefox -name firefox"
# ).split("\n")[-1]
# self.driver = webdriver.Firefox(
# options=firefox_options, service=firefox_services
# )
raise e
print("The driver is launched!")
# Ajout de l'enregistrement
if self.config.record_mode:
@ -152,66 +327,100 @@ class SeleniumLib(object):
# Install DarkReader to help my eyes
# TODO do a script to check if it's the last version
if not self.config.no_dark_mode:
self.driver.install_addon(
"./script/selenium/darkreader-firefox.xpi", temporary=True
)
self.driver.install_addon(
"./script/selenium/odoo_debug-4.0.xpi", temporary=True
)
if not self.config.not_private_mode and not self.config.no_dark_mode:
self.driver.get("about:addons")
# Enable Dark Reader into incognito
self.click("/html/body/div/div[1]/categories-box/button[2]")
self.click(
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card/div/div/div/div/button",
)
self.click(
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card/div/addon-options/panel-list/panel-item[5]",
)
# Enable Run in Private Windows
try:
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[6]/div/label[1]/input",
if self.config.use_firefox_driver and not self.config.use_network:
if not self.config.no_dark_mode:
self.driver.install_addon(
"./script/selenium/darkreader-firefox.xpi", temporary=True
)
except Exception:
# For old version before Firefox 138
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[5]/div/label[1]/input",
self.driver.install_addon(
"./script/selenium/odoo_debug-4.0.xpi", temporary=True
)
# Enable Odoo_debug into incognito
# Back
self.click(
"/html/body/div/div[2]/addon-page-header/div/div[2]/button"
)
self.click(
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card[2]/div/div/div/div/button",
)
self.click(
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card[2]/div/addon-options/panel-list/panel-item[5]",
)
# Enable Run in Private Windows
try:
if (
not self.config.not_private_mode
and not self.config.no_dark_mode
):
self.driver.get("about:addons")
# Enable Dark Reader into incognito
self.click("/html/body/div/div[1]/categories-box/button[2]")
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[6]/div/label[1]/input",
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card/div/div/div/div/button",
)
except Exception:
# For old version before Firefox 138
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[5]/div/label[1]/input",
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card/div/addon-options/panel-list/panel-item[5]",
)
# Enable Run in Private Windows
try:
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[6]/div/label[1]/input",
)
except Exception:
# For old version before Firefox 138
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[5]/div/label[1]/input",
)
# Enable Odoo_debug into incognito
# Back
self.click(
"/html/body/div/div[2]/addon-page-header/div/div[2]/button"
)
self.click(
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card[2]/div/div/div/div/button",
)
self.click(
"/html/body/div/div[2]/div/addon-list/section[1]/addon-card[2]/div/addon-options/panel-list/panel-item[5]",
)
# Enable Run in Private Windows
try:
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[6]/div/label[1]/input",
)
except Exception:
# For old version before Firefox 138
self.click(
"/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[5]/div/label[1]/input",
)
# Ouvrez la page web
if not ignore_open_web:
self.driver.get(f"{self.config.url}/web")
# Close tab opening by DarkReader
if self.config.not_private_mode and not self.config.no_dark_mode:
self.driver.switch_to.window(self.driver.window_handles[-1])
self.driver.close()
self.driver.switch_to.window(self.driver.window_handles[0])
if self.config.use_firefox_driver and not self.config.use_network:
if self.config.not_private_mode and not self.config.no_dark_mode:
self.driver.switch_to.window(self.driver.window_handles[-1])
self.driver.close()
self.driver.switch_to.window(self.driver.window_handles[0])
def get_kdbx(self):
if self.kdbx:
return self.kdbx
print("Open KDBX")
# Open file
chemin_fichier_kdbx = self.config_file.get_config(["kdbx", "path"])
if not chemin_fichier_kdbx:
root = tk.Tk()
root.withdraw() # Hide the main window
chemin_fichier_kdbx = filedialog.askopenfilename(
title="Select a File",
filetypes=(("KeepassX files", "*.kdbx"),),
)
if not chemin_fichier_kdbx:
# _logger.error(f"KDBX is not configured, please fill {CONFIG_FILE}")
return
mot_de_passe_kdbx = self.config_file.get_config(["kdbx", "password"])
if not mot_de_passe_kdbx:
mot_de_passe_kdbx = getpass.getpass(
prompt="Entrez votre mot de passe : "
)
kp = PyKeePass(chemin_fichier_kdbx, password=mot_de_passe_kdbx)
if kp:
self.kdbx = kp
return kp
def get_kdbx(self):
if self.kdbx:
@ -283,17 +492,46 @@ class SeleniumLib(object):
button.click()
return button
def get_element(self, by: str = By.ID, value: str = None, timeout=5):
def get_element(
self,
by: str = By.ID,
value: str = None,
timeout=5,
wait_clickable=False,
is_visible=True,
):
only_one = False
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.visibility_of_any_elements_located(
(
by,
value,
if wait_clickable:
ele = wait.until(
EC.element_to_be_clickable(
(
by,
value,
)
)
)
)
elif is_visible:
ele = wait.until(
EC.visibility_of_any_elements_located(
(
by,
value,
)
)
)
else:
only_one = True
ele = wait.until(
EC.presence_of_element_located(
(
by,
value,
)
)
)
if only_one:
return ele
return ele[0]
def get_element_not_visible(
@ -431,13 +669,14 @@ class SeleniumLib(object):
viewport_ele_value: str = None,
element: WebElement = None,
with_index: bool = False,
is_visible=True,
index_of_list: int = 0,
):
# ele = self.driver.find_element(by, value)
if element:
ele = element
else:
ele = self.get_element(by, value, timeout)
ele = self.get_element(by, value, timeout, is_visible=is_visible)
if not no_scroll:
viewport_ele = None
if viewport_ele_value:
@ -742,40 +981,40 @@ class SeleniumLib(object):
# Website
def odoo_show_robot_message(
self, msg="Ho no!", hide_message_after_x_seconds=0
self, msg="Ho no!", hide_message_after_x_seconds=0, robot_base64=None
):
# Injecter l'animation SVG dans la page
script_text = ""
if msg:
msg_to_print = msg.replace("'", "\\'")
script_text = f"""
// Ajouter le texte
const textElement = document.createElement('div');
textElement.innerText = '{msg_to_print}';
textElement.style.color = '#FFFFFF';
textElement.style.fontSize = '24px';
textElement.style.marginBottom = '20px';
textElement.style.fontWeight = 'bold';
lst_mst_to_print = msg_to_print.split("\n")
script_text = ""
for index, line in enumerate(lst_mst_to_print):
script_text += f"""
// Ajouter le texte
const textElement_{index} = document.createElement('div');
textElement_{index}.textContent = '{line}';
textElement_{index}.style.color = '#FFFFFF';
textElement_{index}.style.fontSize = '24px';
textElement_{index}.style.marginBottom = '20px';
textElement_{index}.style.fontWeight = 'bold';
overlay.appendChild(textElement);
"""
script = (
"""
(function() {
// Créer un div pour l'overlay
const overlay = document.createElement('div');
overlay.id = 'uniqueOverlay'; // Ajout de l'ID
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '9999';
overlay.appendChild(textElement_{index});
"""
if robot_base64:
script_robot = ""
script_add_robot = f"""
const img = new Image();
img.alt = 'Robot';
img.className = 'cute-robot';
img.style.width = '150px';
img.style.cursor = 'pointer';
img.src = '{robot_base64}';
overlay.appendChild(img);
"""
else:
script_robot = """
// Contenu SVG du robot
const svgContent = `
<svg class="cute-robot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 150" style="width: 150px; cursor: pointer;">
@ -801,11 +1040,32 @@ class SeleniumLib(object):
<rect x="50" y="120" width="15" height="30" rx="5" ry="5" fill="#388E3C"/>
</svg>
`;
"""
script_add_robot = """
// Ajouter le contenu SVG au div
overlay.innerHTML += svgContent;
"""
script = (
"""
(function() {
// Créer un div pour l'overlay
const overlay = document.createElement('div');
overlay.id = 'uniqueOverlay'; // Ajout de l'ID
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '9999';
"""
+ script_robot
+ script_text
+ script_add_robot
+ """
// Ajouter le contenu SVG au div
overlay.innerHTML += svgContent;
// Ajouter un gestionnaire de clic pour supprimer l'overlay
overlay.onclick = function() {
@ -856,7 +1116,7 @@ class SeleniumLib(object):
def odoo_web_click_principal_menu(self):
# Click menu button
if self.odoo_version == "16.0":
if self.odoo_version in ["16.0", "17.0", "18.0"]:
button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'dropdown-toggle') and"
@ -869,6 +1129,119 @@ class SeleniumLib(object):
)
return button
def odoo_web_click_item_list(self, index=0, field_name="name"):
# Click item list
all_row = self.get_all_element(
by=By.XPATH,
value=f"//tr[contains(@class, 'o_data_row')]/td[contains(@name, '{field_name}')]",
timeout=10,
)
row = all_row[index] if index < len(all_row) else all_row[-1]
button = self.click_with_mouse_move(element=row)
return button
def odoo_web_click_open_search(self):
# Click search button
button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'o_searchview_dropdown_toggler')]",
timeout=10,
)
return button
def odoo_web_click_open_search_add_group_custom(self, group_name):
# Add personalize group
button = self.click_with_mouse_move(
By.XPATH,
value="//select[contains(@class, 'o_add_custom_group_menu')]",
timeout=10,
)
if self.wait_human_time:
time.sleep(1)
select_elem = self.get_element(
By.XPATH,
value="//select[contains(@class, 'o_add_custom_group_menu')]",
timeout=10,
wait_clickable=False,
)
select_obj = Select(select_elem)
# (ex: <option value="customer">Client</option>)
select_obj.select_by_value(group_name)
return select_elem
def odoo_web_click_open_search_filter(
self,
filter_label=None,
filter_sub_label=None,
lst_update_condition=None,
):
if not lst_update_condition:
lst_update_condition = []
if filter_label:
button = self.click_with_mouse_move(
By.XPATH,
value=f"//button[contains(@class,'o_menu_item') and contains(normalize-space(.), '{filter_label}')]",
timeout=10,
)
if filter_sub_label:
button = self.click_with_mouse_move(
By.XPATH,
value=f"//span[contains(@class,'o_item_option') and contains(normalize-space(.), '{filter_sub_label}')]",
timeout=10,
)
if lst_update_condition:
filter_text = f"{filter_label}: {filter_sub_label}"
button = self.click_with_mouse_move(
By.XPATH,
value=f"//small[contains(@class,'o_facet_value') and contains(normalize-space(.), '{filter_text}')]/../..//i[contains(@class,'fa-cog')]",
timeout=10,
is_visible=False,
)
time.sleep(1)
for item_update_condition in lst_update_condition:
condition_value = item_update_condition.get("condition")
if condition_value:
select_elem = self.click_with_mouse_move(
By.XPATH,
value=f"//div[contains(@class,'modal-content')]//select[contains(@class,'pe-3')]",
timeout=10,
is_visible=False,
)
select_obj = Select(select_elem)
# (ex: <option value="customer">Client</option>)
select_obj.select_by_value(condition_value)
else:
_logger.error(
f"Not supported item_update_condition '{item_update_condition}'"
)
# Save
button = self.click_with_mouse_move(
By.XPATH,
value=f"//footer[contains(@class,'modal-footer')]//button[contains(@class,'btn-primary') and contains(normalize-space(.), 'Confirmer')]",
timeout=10,
)
def odoo_fill_search_input(self, text, selection_research=None):
input_elem = self.click_with_mouse_move(
By.XPATH,
value="//input[contains(@class, 'o_searchview_input')]",
timeout=10,
)
input_elem.send_keys(text)
time.sleep(1)
if not selection_research:
input_elem.send_keys(Keys.ENTER)
else:
input_autocomplete_elem = self.click_with_mouse_move(
By.XPATH,
value="//ul[contains(@class,'o_searchview_autocomplete')]"
f"//li[contains(@class,'o_menu_item') and .//b[normalize-space(.)='{selection_research}']]"
"//a[not(contains(@class,'o_expand'))]",
)
return input_elem
def odoo_web_principal_menu_click_root_menu(self, from_text=""):
if not from_text:
raise Exception(
@ -952,6 +1325,23 @@ class SeleniumLib(object):
"o_form_button_save"
)
def odoo_web_change_view(
self, view_name="list", raise_error=False, timeout=5
):
if view_name not in ["list", "kanban", "timeline"]:
msg = f"odoo_web_change_view view_name '{view_name}' invalid"
if raise_error:
raise Exception(msg)
_logger.error(msg)
return None
status_button = self.click_with_mouse_move(
by=By.XPATH,
value=f"//nav[contains(@class, 'o_cp_switch_buttons')]/button[contains(@class, 'o_{view_name}')]",
)
print(f"Change view '{view_name}' cliqué.")
return status_button
def odoo_web_form_click_web_publish_action(self):
return self.odoo_web_form_click_button_action("website_publish_button")
@ -1059,14 +1449,20 @@ class SeleniumLib(object):
checkbox_div.click()
# TODO improve speed by observation of comportement
time.sleep(0.5)
checkbox_date_div = self.driver.find_element(
By.CSS_SELECTOR, ".bootstrap-datetimepicker-widget"
)
# time.sleep(0.5)
str_date_today = date_to_select.strftime("%Y-%m-%d")
new_value = checkbox_date_div.find_element(
By.XPATH, f"//td[@data-day='{str_date_today}']"
)
if self.odoo_version in ["17.0", "18.0"]:
checkbox_date_div = self.driver.find_element(
By.CSS_SELECTOR, ".o_datetime_picker"
)
new_value = self.select_date(self.driver, date_to_select)
else:
checkbox_date_div = self.driver.find_element(
By.CSS_SELECTOR, ".bootstrap-datetimepicker-widget"
)
# time.sleep(0.5)
new_value = checkbox_date_div.find_element(
By.XPATH, f"//td[@data-day='{str_date_today}']"
)
new_value.click()
return new_value
@ -1108,6 +1504,9 @@ class SeleniumLib(object):
time.sleep(var_wait_time)
self.video_recorder.start()
def quit(self):
self.driver.quit()
def stop_record(self):
time.sleep(self.config.selenium_default_delay_presentation)
@ -1122,10 +1521,13 @@ class SeleniumLib(object):
".webm", ".mp4"
)
if self.config.selenium_video_auto_convert_mp4:
os.popen(
"ffmpeg -i"
f" {self.filename_recording} {filename_recording_mp4}"
)
with_auto_fix = True
if with_auto_fix:
cmd = f'ffmpeg -i {self.filename_recording} -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -pix_fmt yuv420p -crf 23 -preset veryfast -c:a aac -b:a 160k -movflags +faststart {filename_recording_mp4}'
else:
cmd = f"ffmpeg -i {self.filename_recording} {filename_recording_mp4}"
print(cmd)
os.popen(cmd)
if self.config.selenium_video_auto_play_video:
print(f"Play video {self.filename_recording}")
@ -1193,6 +1595,94 @@ class SeleniumLib(object):
return date_naissance.strftime("%Y-%m-%d")
def _get_visible_month_year(self, picker):
header = picker.find_element(
By.CSS_SELECTOR, ".o_datetime_picker_header .o_header_part"
)
text = header.text.strip().lower() # ex: "octobre 2025"
parts = text.split()
# s'il y a des espaces insécables ou formats bizarres
mois_txt = parts[0].replace("\xa0", " ")
annee = int(parts[-1])
mois = MONTHS_FR[mois_txt]
return (
mois,
annee,
header.text,
) # on retourne aussi le texte exact pour attendre le changement
def select_date(self, driver, date_to_select):
# 1) ouvrir/viser le datepicker (si besoin, clique l'input avant)
picker = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, ".o_datetime_picker")
)
)
# 2) naviguer au bon mois
max_hops = 24 # sécurité (2 ans)
for _ in range(max_hops):
mois_vis, annee_vis, header_txt = self._get_visible_month_year(
picker
)
if (annee_vis, mois_vis) == (
date_to_select.year,
date_to_select.month,
):
break
# bouton suivant/précédent dans l'entête du picker courant
if (annee_vis, mois_vis) < (
date_to_select.year,
date_to_select.month,
):
btn = picker.find_element(
By.CSS_SELECTOR, ".o_datetime_picker_header .o_next"
)
else:
btn = picker.find_element(
By.CSS_SELECTOR, ".o_datetime_picker_header .o_previous"
)
btn.click()
# attendre que l'en-tête change pour éviter les doubles clics trop rapides
WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element(
(
By.CSS_SELECTOR,
".o_datetime_picker_header .o_header_part",
),
"",
)
)
WebDriverWait(driver, 10).until(
lambda d: picker.find_element(
By.CSS_SELECTOR, ".o_datetime_picker_header .o_header_part"
).text
!= header_txt
)
else:
raise RuntimeError(
"Impossible d'atteindre le mois voulu dans le délai imparti."
)
# 3) cliquer le jour (les jours cliquables sont des <button> avec un <span> = numéro)
day = str(int(date_to_select.day)) # sans zéro en tête
day_xpath = f".//button[contains(@class,'o_date_item_cell') and .//span[normalize-space(text())='{day}']]"
day_btn = WebDriverWait(picker, 10).until(
EC.element_to_be_clickable((By.XPATH, day_xpath))
)
return day_btn
def get_args(parser):
args = parser.parse_args()
if args.use_chrome_driver:
args.use_firefox_driver = False
return args
def fill_parser(parser):
parser.add_argument(
@ -1209,12 +1699,37 @@ def fill_parser(parser):
help="Pause at the end of an action.",
)
group_animation.add_argument(
"--human_time_speed",
action="store_true",
help="Will wait after each action to help human to follow",
)
group_browser = parser.add_argument_group(title="Browser")
group_browser.add_argument(
"--not_private_mode",
action="store_true",
help="Default is private mode.",
)
group_browser.add_argument(
"--use_chrome_driver",
action="store_true",
help="Switch to chrome instead of great Firefox.",
)
group_browser.add_argument(
"--use_firefox_driver",
action="store_true",
default=True,
help="The default is firefox.",
)
group_browser.add_argument(
"--use_network",
help="Specify the adress, example: http://localhost:4444",
)
group_browser.add_argument(
"--window_size",
help="Example 1920,1080",
)
group_browser.add_argument(
"--headless",
action="store_true",

View file

@ -2,10 +2,15 @@
import threading
import gi
try:
import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst
except ImportError:
print("Please read installation script/selenium/install.md")
raise
gi.require_version("Gst", "1.0")
from gi.repository import Gst
Gst.init(None)

View file

@ -7,9 +7,15 @@ import os
import sys
import time
import selenium_lib
new_path = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..")
)
sys.path.append(new_path)
from selenium.webdriver.common.by import By
from script.selenium import selenium_lib
def fill_parser(parser):
group_login = parser.add_argument_group(title="Login")
@ -30,13 +36,24 @@ def fill_parser(parser):
)
def run(config, selenium_tool):
def run(
config,
selenium_tool,
default_email_auth=None,
default_password_auth=None,
no_try_test=False,
):
# Trouvez les éléments du formulaire
courriel_input = selenium_tool.get_element(by=By.NAME, value="login")
mot_de_passe_input = selenium_tool.get_element(by=By.NAME, value="password")
mot_de_passe_input = selenium_tool.get_element(
by=By.NAME, value="password"
)
# div_connexion_button = selenium_tool.get_element(by=By.CLASS_NAME, value="oe_login_buttons")
connexion_button = selenium_tool.get_element(by=By.CSS_SELECTOR, value="[type='submit']")
connexion_button = selenium_tool.get_element(
by=By.CSS_SELECTOR, value="[type='submit']"
)
actual_url = selenium_tool.driver.current_url
# try:
# connexion_button = selenium_tool.driver.find_element(
# By.XPATH,
@ -51,8 +68,18 @@ def run(config, selenium_tool):
# )
# Remplissez le courriel et le mot de passe
courriel_input.send_keys(config.default_email_auth)
mot_de_passe_input.send_keys(config.default_password_auth)
email_auth = (
default_email_auth if default_email_auth else config.default_email_auth
)
pass_auth = (
default_password_auth
if default_password_auth
else config.default_password_auth
)
courriel_input.clear()
mot_de_passe_input.clear()
courriel_input.send_keys(email_auth)
mot_de_passe_input.send_keys(pass_auth)
# Cliquez sur le bouton "Connexion"
try:
@ -100,6 +127,21 @@ def run(config, selenium_tool):
# )
# )
if not no_try_test:
time.sleep(1)
if actual_url in [
selenium_tool.driver.current_url,
f"{selenium_tool.driver.current_url}?redirect=%2Fweb%3F",
]:
run(
config,
selenium_tool,
default_email_auth="test",
default_password_auth="test",
no_try_test=True,
)
return
# Remove chatbot if open, because will crash wizard div[4] (to div[5])
selenium_tool.check_bot_chat_and_close()