erplibre/script/selenium/selenium_lib.py

2165 lines
77 KiB
Python

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# 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 subprocess
import sys
import tempfile
import time
import tkinter as tk
from pathlib import Path
from tkinter import filedialog
from pykeepass import PyKeePass
from randomwordfr import RandomWordFr
from selenium import webdriver
from selenium.common.exceptions import (
ElementClickInterceptedException,
TimeoutException,
)
from selenium.webdriver import ActionChains
from selenium.webdriver.common.actions.wheel_input import ScrollOrigin
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
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 Select, WebDriverWait
new_path = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..")
)
sys.path.append(new_path)
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__)
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,
}
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_copy_download_dir_path = None
self.defaut_timeout = 60
date_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
tmp_dir = tempfile.mkdtemp(
prefix=f"selenium_download_{date_str}_",
# dir="/tmp/test/",
)
self.default_download_dir_path = tmp_dir
if self.config.video_suffix:
self.filename_recording = (
f"video_{self.config.video_suffix}_"
+ time.strftime("%Y_%m_%d-%H_%M_%S")
+ ".webm"
)
else:
self.filename_recording = (
"video_" + time.strftime("%Y_%m_%d-%H_%M_%S") + ".webm"
)
dir_path_screencast = os.path.join(".", "screencasts")
self.filename_recording = os.path.join(
dir_path_screencast, self.filename_recording
)
self.dirname_recording = dir_path_screencast
self.driver = None
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:
dir_path = os.path.join(".", "screenshots")
if not os.path.isdir(dir_path):
os.mkdir(dir_path)
file_path = os.path.join(
dir_path,
f"{self.config.scenario}_{str(int(time.time() * 10000))}.png",
)
self.driver.save_screenshot(file_path)
def configure(self, ignore_open_web=False):
# Configuration pour lancer Firefox en mode de navigation privée
firefox_options = webdriver.FirefoxOptions()
if not self.config.not_private_mode:
firefox_options.add_argument("--private")
if self.config.headless:
firefox_options.add_argument("--headless")
# firefox_options.set_preference("browser.link.open_newwindow", 3)
# firefox_options.set_preference("browser.link.open_newwindow.restriction", 2)
# Définissez la préférence pour les notifications de bureau
# 1 signifie "autoriser", 2 signifie "bloquer"
firefox_options.set_preference(
"permissions.default.desktop-notification", 1
)
firefox_options.set_preference("browser.download.folderList", 2)
firefox_options.set_preference("browser.download.useDownloadDir", True)
firefox_options.set_preference(
"browser.download.manager.showWhenStarting", False
)
if self.default_download_dir_path:
firefox_options.set_preference(
"browser.download.dir", self.default_download_dir_path
)
firefox_options.set_preference(
"browser.helperApps.neverAsk.saveToDisk",
"application/pdf,"
"application/x-pdf,"
"application/vnd.ms-excel,"
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,"
"text/csv,"
"application/octet-stream",
)
# disable pdf viewer from firefox
firefox_options.set_preference("pdfjs.disabled", True)
firefox_services = None
if self.config.firefox_binary_path:
firefox_services = Service(
executable_path=self.config.firefox_binary_path
)
if self.config.gecko_binary_path:
firefox_options.binary_location = self.config.gecko_binary_path
# Créez une instance du navigateur Firefox avec les options de navigation privée
try:
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
)
print(f"INFO: Result 'whoami': {whoami_result.stdout.strip()}")
except Exception as e:
print(f"ERROR when check user: {e}")
driver_url = f"{self.config.use_network}/wd/hub"
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"
)
chrome_options.enable_downloads = True
self.driver = webdriver.Remote(
options=chrome_options,
command_executor=driver_url,
)
else:
from webdriver_manager.chrome import ChromeDriverManager
service = Service(
ChromeDriverManager().install(),
log_path="/tmp/chromedriver.log",
)
chrome_options.enable_downloads = True
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:
# test snap firefox for Ubuntu first
snap_firefox_path = (
"/snap/firefox/current/usr/lib/firefox/firefox"
)
firefox_path_exist = Path(snap_firefox_path).exists()
if firefox_path_exist:
firefox_options.binary_location = snap_firefox_path
else:
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
)
if self.default_download_dir_path:
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}"
# )
width, height = self.config.window_size.split(",")
firefox_options.add_argument("--width=" + str(width))
firefox_options.add_argument("--height=" + str(height))
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"
)
firefox_options.enable_downloads = True
self.driver = webdriver.Remote(
options=firefox_options,
command_executor=driver_url,
)
else:
firefox_options.add_argument("-remote-allow-system-access")
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."
)
# 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:
# Create recording
from script.selenium.selenium_video import VideoRecorder
if not os.path.isdir(self.dirname_recording):
os.mkdir(self.dirname_recording)
self.video_recorder = VideoRecorder(
self.driver, filename=self.filename_recording
)
# import vlc
# Install DarkReader to help my eyes
# TODO do a script to check if it's the last version
if self.config.use_firefox_driver and not self.config.use_network:
if not self.config.no_dark_mode:
self.driver.install_addon(
os.path.join(
new_path, "script/selenium/darkreader-firefox.xpi"
),
temporary=True,
)
self.driver.install_addon(
os.path.join(
new_path, "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.set_context("chrome")
self.driver.get("about:addons")
# Enable Dark Reader into incognito
# Click Extensions
self.click('//button[@viewid="addons://list/extension"]')
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",
)
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.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_value(
["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_value(
["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
@staticmethod
def get_french_word_no_space_no_accent():
word = ""
rw = RandomWordFr()
while not word or " " in word:
word = rw.get().get("word")
if not re.match("^[a-zA-Z_]+$", word):
word = ""
return word
def click(self, xpath, timeout=None):
if timeout is None:
timeout = self.defaut_timeout
wait = WebDriverWait(self.driver, timeout)
button = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
xpath,
)
)
)
button.click()
return button
def get_elements(self, by: str = By.ID, value: str = None, timeout=None):
if timeout is None:
timeout = self.defaut_timeout
wait = WebDriverWait(self.driver, timeout)
elements = wait.until(
EC.visibility_of_any_elements_located(
(
by,
value,
)
)
)
return elements
def get_element(
self,
by: str = By.ID,
value: str = None,
timeout=None,
wait_clickable=False,
wait_is_invisible=False,
is_visible=True,
):
if timeout is None:
timeout = self.defaut_timeout
only_one = False
wait = WebDriverWait(self.driver, timeout)
if wait_clickable:
ele = wait.until(
EC.element_to_be_clickable(
(
by,
value,
)
)
)
only_one = True
elif is_visible:
ele = wait.until(
EC.visibility_of_any_elements_located(
(
by,
value,
)
)
)
elif wait_is_invisible:
ele = wait.until(
EC.invisibility_of_element_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(
self, by: str = By.ID, value: str = None, timeout=None
):
if timeout is None:
timeout = self.defaut_timeout
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.presence_of_element_located(
(
by,
value,
)
)
)
return ele
def get_elements_not_visible(
self, by: str = By.ID, value: str = None, timeout=None
):
if timeout is None:
timeout = self.defaut_timeout
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.presence_of_all_elements_located(
(
by,
value,
)
)
)
return ele
def get_text_from_element(
self, by: str = By.ID, value: str = None, timeout: int = None
):
if timeout is None:
timeout = self.defaut_timeout
ele = self.get_element(by, value, timeout=timeout)
return ele.text
def get_all_element(
self, by: str = By.ID, value: str = None, timeout=None
):
if timeout is None:
timeout = self.defaut_timeout
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.visibility_of_all_elements_located(
(
by,
value,
)
)
)
return ele
def wait_new_element_and_click(
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=None,
inject_cursor=False,
):
if timeout is None:
timeout = self.defaut_timeout
new_element = self.wait_new_element(
by=by, value=value, delay_wait=delay_wait, timeout=timeout
)
if new_element:
if inject_cursor:
self.inject_cursor()
self.click_with_mouse_move(element=new_element, no_scroll=True)
return new_element
def wait_new_element(
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=None,
timeout_wait=None,
):
if timeout is None:
timeout = self.defaut_timeout
element = None
# TODO support timeout_wait
while element is None:
time.sleep(delay_wait)
try:
element = self.get_element(by, value, timeout)
except TimeoutException as e:
pass
print(f"Waiting {delay_wait} seconds after value '{value}'")
return element
def wait_add_new_element_and_click(
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=None,
):
if timeout is None:
timeout = self.defaut_timeout
new_element = self.wait_add_new_element(
by=by, value=value, delay_wait=delay_wait, timeout=timeout
)
self.click_with_mouse_move(element=new_element, no_scroll=True)
return new_element
def wait_add_new_element(
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=None,
):
if timeout is None:
timeout = self.defaut_timeout
all_element_init = self.get_all_element(by=by, value=value)
len_all_element_init = len(all_element_init)
len_all_element = len_all_element_init
all_element = []
while len_all_element_init == len_all_element:
time.sleep(delay_wait)
all_element = self.get_all_element(by=by, value=value)
len_all_element = len(all_element)
print(f"Waiting {delay_wait} seconds after value '{value}'")
new_element = all_element[-1]
return new_element
def wait_increment_number_text_and_click(
self,
by: str = By.ID,
value: str = None,
delay_wait=1,
timeout=None,
):
if timeout is None:
timeout = self.defaut_timeout
element_init = self.get_element(by=by, value=value, timeout=timeout)
actual_number = int(element_init.text)
number_goal = actual_number + 1
while actual_number < number_goal:
time.sleep(delay_wait)
element_goal = self.get_element(
by=by, value=value, timeout=timeout
)
actual_number = int(element_goal.text)
print(f"Waiting {delay_wait} seconds after value '{value}'")
self.click_with_mouse_move(element=element_init, no_scroll=True)
return element_init
def click_with_mouse_move(
self,
by: str = By.ID,
value: str = None,
timeout: int = None,
no_scroll: bool = False,
viewport_ele_by: str = By.ID,
viewport_ele_value: str = None,
element: WebElement = None,
with_index: bool = False,
is_visible=True,
index_of_list: int = 0,
support_multiple_click: bool = False,
):
if timeout is None:
timeout = self.defaut_timeout
# Exemple Detect button by text value
# Replace CLASS_NAME
# By.CSS_SELECTOR, "input.CLASS_NAME[value='Fermer']"
# ele = self.driver.find_element(by, value)
if element:
ele = element
else:
if support_multiple_click:
ele = self.get_elements(by, value, timeout)
else:
ele = self.get_element(
by, value, timeout, is_visible=is_visible
)
if not no_scroll:
viewport_ele = None
if viewport_ele_value:
viewport_ele = self.get_element(
viewport_ele_by, viewport_ele_value, timeout
)
if support_multiple_click:
for one_ele in ele:
self.scrollto_element(one_ele, viewport_ele=viewport_ele)
# ActionChains(self.driver).move_to_element(one_ele).click().perform()
ActionChains(self.driver).move_to_element(
one_ele
).perform()
else:
self.scrollto_element(ele, viewport_ele=viewport_ele)
# ActionChains(self.driver).move_to_element(ele).click().perform()
ActionChains(self.driver).move_to_element(ele).perform()
time.sleep(self.config.selenium_default_delay)
wait = WebDriverWait(self.driver, timeout)
if element:
button = wait.until(EC.element_to_be_clickable(element))
else:
if with_index:
buttons = wait.until(
EC.presence_of_all_elements_located(
(
by,
value,
)
)
)
button = buttons[index_of_list]
else:
if support_multiple_click:
button = wait.until(
# EC.visibility_of_all_elements_located(
EC.presence_of_all_elements_located(
(
by,
value,
)
)
)
else:
button = wait.until(
EC.element_to_be_clickable(
(
by,
value,
)
)
)
try:
if support_multiple_click:
for btn in button:
try:
btn.click()
# This button work!
return btn
except Exception as e:
pass
else:
button.click()
except ElementClickInterceptedException as e:
try:
print(e)
if support_multiple_click:
for btn in button:
self.driver.execute_script(
"arguments[0].scrollIntoView(true);", btn
)
btn.click()
else:
self.driver.execute_script(
"arguments[0].scrollIntoView(true);", button
)
button.click()
except ElementClickInterceptedException as e:
print(e)
if support_multiple_click:
for btn in button:
self.driver.execute_script(
"arguments[0].click();", btn
)
else:
self.driver.execute_script("arguments[0].click();", button)
return button
def click_canvas_form(
self,
by: str = By.ID,
value: str = None,
timeout: int = None,
form="carre",
):
if timeout is None:
timeout = self.defaut_timeout
# ele = self.driver.find_element(by, value)
ele = self.get_element(by, value, timeout)
actions = ActionChains(self.driver)
actions.move_to_element_with_offset(ele, -50, -50)
actions.click_and_hold()
if form == "carre":
actions.move_by_offset(50, 0)
actions.move_by_offset(0, 50)
actions.move_by_offset(-50, 0)
actions.move_by_offset(0, -50)
actions.release()
actions.perform()
def input_text_with_mouse_move(
self,
by: str = By.ID,
value: str = None,
text_value: str = "",
delay_after: int = -1,
timeout: int = None,
no_scroll: bool = False,
viewport_ele_by: str = By.ID,
viewport_ele_value: str = None,
clear_before: bool = False,
click_before: bool = False,
):
if timeout is None:
timeout = self.defaut_timeout
# ele = self.driver.find_element(by, value)
ele = self.get_element(by, value)
if not no_scroll:
viewport_ele = None
if viewport_ele_value:
viewport_ele = self.get_element(
viewport_ele_by, viewport_ele_value, timeout
)
self.scrollto_element(ele, viewport_ele=viewport_ele)
ActionChains(self.driver).move_to_element(ele).perform()
# Write content
if click_before:
# Need this to update a datetimepicker
ele.click()
time.sleep(0.3)
if clear_before:
ele.clear()
if click_before:
# Need this to update a datetimepicker
ele.click()
time.sleep(0.3)
ele.send_keys(text_value)
if delay_after > 0:
time.sleep(delay_after)
elif self.config.selenium_default_delay > 0:
time.sleep(self.config.selenium_default_delay)
def scrollto_element1(self, element_name):
# ele = self.driver.find_element(By.TAG_NAME, element_name)
ele = self.get_element(By.TAG_NAME, element_name)
# delta_y = ele.rect["y"]
ActionChains(self.driver).scroll_to_element(ele).perform()
# ele = self.get_element(By.TAG_NAME, element_name)
# delta_y = ele.rect["y"]
# ActionChains(self.driver).scroll_by_amount(0, delta_y).perform()
time.sleep(0.5)
def scrollto_xpath5(self, xpath):
footer = self.driver.find_element(By.XPATH, xpath)
# footer = self.get_element(By.XPATH, xpath)
delta_y = footer.rect["y"]
ActionChains(self.driver).scroll_by_amount(0, int(delta_y)).perform()
time.sleep(0.5)
def scrollto_xpath2(self, xpath):
footer = self.driver.find_element(By.XPATH, xpath)
# footer = self.get_element(By.XPATH, xpath)
scroll_origin = ScrollOrigin.from_element(footer)
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, 100
).perform()
time.sleep(0.5)
def scrollto_xpath3(self, xpath):
footer = self.driver.find_element(By.XPATH, xpath)
# footer = self.get_element(By.XPATH, xpath)
scroll_origin = ScrollOrigin.from_element(footer)
ActionChains(self.driver).scroll_to_element(scroll_origin).perform()
time.sleep(0.5)
def scrollto_xpath(
self,
xpath,
offset_middle=True,
smooth_scroll=True,
smooth_speed=50,
smooth_fps=1 / 25,
):
footer = self.driver.find_element(By.XPATH, xpath)
# footer = self.get_element(By.XPATH, xpath)
scroll_origin = ScrollOrigin.from_viewport(10, 10)
delta_y = footer.rect["y"]
if offset_middle:
# TODO check size windows and divide by 2
delta_y -= 400
print(f"scroll to xpath at y:{delta_y} position")
if smooth_scroll:
actual_pos = 0
while delta_y > actual_pos:
actual_pos += smooth_speed
print(actual_pos)
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(smooth_speed)
).perform()
print(f"Wait {smooth_fps} seconds")
time.sleep(smooth_fps)
print("end")
else:
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(delta_y)
).perform()
time.sleep(0.5)
def scrollto_element(
self,
element,
smooth_scroll=True,
smooth_speed=50,
smooth_fps=1 / 25,
viewport_ele=None,
):
window_y = self.driver.execute_script(
"return window.scrollY || window.pageYOffset;"
)
window_height = self.driver.get_window_size().get("height")
element_height = element.rect["height"]
viewport_height = self.driver.execute_script(
"return window.innerHeight || document.documentElement.clientHeight;"
)
if viewport_ele:
scroll_origin = ScrollOrigin.from_element(viewport_ele, 10, 10)
else:
scroll_origin = ScrollOrigin.from_viewport(10, 10)
delta_y = element.rect["y"]
target_distance = (
delta_y - (viewport_height - element_height) / 2 - window_y
)
if window_y < delta_y < window_y + viewport_height - 50:
# No need to scroll
return
print(f"scroll to xpath at y:{delta_y} position")
if smooth_scroll:
actual_pos = 0
# Detect go top or bot
if delta_y > window_y:
step = abs(int(smooth_speed)) # to the top
condition = lambda a, d: a < d
go_down = True
else:
step = -abs(int(smooth_speed)) # to the bot
condition = lambda a, d: a > d
go_down = False
while condition(actual_pos, target_distance):
# TODO detect when cannot scroll, stop scrolling
actual_pos += step
if go_down and actual_pos > target_distance:
step = actual_pos - target_distance
elif not go_down and actual_pos < target_distance:
step = actual_pos - target_distance
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(step)
).perform()
print(f"Wait {smooth_fps} seconds")
time.sleep(smooth_fps)
print("end")
else:
ActionChains(self.driver).scroll_from_origin(
scroll_origin, 0, int(target_distance)
).perform()
# action = ActionChains(self.driver)
# action.click_and_hold(element).move_by_offset(0, 1).perform()
# # Don't forget to release the click_and_hold, for example with:
# action.reset_actions()
time.sleep(0.5)
def inject_cursor(self):
cursor_script = """
var cursor = document.createElement('div');
cursor.style.position = 'absolute';
cursor.style.zIndex = '9999';
cursor.style.width = '30px';
cursor.style.height = '30px';
cursor.style.borderRadius = '50%';
cursor.style.backgroundColor = 'red';
cursor.style.pointerEvents = 'none';
document.body.appendChild(cursor);
document.addEventListener('mousemove', function(e) {
cursor.style.left = e.pageX - 5 + 'px';
cursor.style.top = e.pageY - 5 + 'px';
});
"""
try:
try:
self.driver.execute_script(cursor_script)
except Exception as e:
time.sleep(1)
print("Error execute script cursor, wait 1 second")
self.driver.execute_script(cursor_script)
except Exception as e:
time.sleep(3)
print("Error execute script cursor, wait 3 second")
self.driver.execute_script(cursor_script)
def scenario_create_new_account_with_random_user(
self, show_cursor=False, def_action_before_submit=None
):
# Click no account
if show_cursor:
self.inject_cursor()
self.click_with_mouse_move(
By.XPATH, "/html/body/div[1]/main/div/form/div[3]/div[1]/a[1]"
)
# Trouvez les éléments du formulaire
if show_cursor:
self.inject_cursor()
# Remplissez le courriel et le mot de passe
first_name = self.get_french_word_no_space_no_accent()
password = first_name.lower()
second_name = self.get_french_word_no_space_no_accent()
full_name = f"{first_name} {second_name}"
domain = self.get_french_word_no_space_no_accent().lower()
self.input_text_with_mouse_move(
By.NAME, "login", f"{password}@{domain}.com"
)
self.input_text_with_mouse_move(By.NAME, "name", full_name)
self.input_text_with_mouse_move(By.NAME, "password", password)
self.input_text_with_mouse_move(By.NAME, "confirm_password", password)
if def_action_before_submit:
def_action_before_submit()
self.click_with_mouse_move(
By.XPATH, "/html/body/div[1]/main/div/form/div[6]/button"
)
def open_tab(self, url):
# Open a new window
self.driver.execute_script("window.open('');")
# Switch to the new window and open new URL
self.driver.switch_to.window(self.driver.window_handles[1])
self.driver.get(url)
def switch_tab(self, index):
# Switch to the new window and open new URL
self.driver.switch_to.window(self.driver.window_handles[index])
def refresh(self, wait_ready=False):
self.driver.refresh()
if wait_ready:
WebDriverWait(self.driver, 15).until(
lambda d: d.execute_script("return document.readyState")
== "complete"
)
def check_bot_chat_and_close(self):
try:
xpath_button = "/html/body/div[3]/div[1]/span[2]/a[2]"
button = self.driver.find_element(By.XPATH, xpath_button)
# button = self.get_element(By.XPATH, xpath_button)
self.click(self.driver, xpath_button)
except Exception:
print("Chatbot cannot be found, stop searching it.")
# Website
def odoo_show_robot_message(
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("'", "\\'")
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_{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;">
<!-- Corps du robot -->
<rect x="25" y="50" width="50" height="70" rx="10" ry="10" fill="#4CAF50"/>
<!-- Tête du robot -->
<circle cx="50" cy="30" r="20" fill="#4CAF50"/>
<!-- Yeux du robot -->
<circle cx="42" cy="25" r="3" fill="#FFFFFF"/>
<circle cx="58" cy="25" r="3" fill="#FFFFFF"/>
<!-- Bouche du robot -->
<path d="M44 38 Q 50 42 56 38" stroke="#FFFFFF" stroke-width="2" fill="transparent"/>
<!-- Bras du robot -->
<rect x="15" y="60" width="15" height="50" rx="5" ry="5" fill="#388E3C"/>
<rect x="70" y="60" width="15" height="50" rx="5" ry="5" fill="#388E3C"/>
<!-- Jambes du robot -->
<rect x="35" y="120" width="15" height="30" rx="5" ry="5" fill="#388E3C"/>
<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 un gestionnaire de clic pour supprimer l'overlay
overlay.onclick = function() {
overlay.style.display = 'none';
};
// Ajouter l'overlay au corps du document
document.body.appendChild(overlay);
})();
"""
)
# Exécuter le script pour injecter l'animation
self.driver.execute_script(script)
if hide_message_after_x_seconds:
time.sleep(hide_message_after_x_seconds)
delete_script = """(function() {
// Sélectionner l'overlay par ses attributs de style uniques
const overlay = document.getElementById('uniqueOverlay');
// Vérifier si l'overlay existe et le supprimer
if (overlay) {
overlay.remove();
}
})();"""
self.driver.execute_script(delete_script)
print(f"Script inject robot svg with msg '{msg}'")
def odoo_website_menu_click(self, from_text=""):
if not from_text:
raise Exception(
"Cannot click, empty parameter from method"
" odoo_website_menu_click"
)
button = self.click_with_mouse_move(
By.LINK_TEXT, from_text, timeout=self.defaut_timeout
)
return button
# Web
def odoo_fill_widget_char(self, field_name, text):
# Type input
text_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.NAME, field_name))
)
text_field.send_keys(text)
return text_field
def odoo_web_click_principal_menu(self):
# Click menu button
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"
" .//i[contains(@class, 'oi-apps')]]",
)
elif self.odoo_version == "14.0":
button = self.click_with_mouse_move(
By.CSS_SELECTOR, "a.full[data-toggle='dropdown']"
)
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}')]",
)
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')]",
)
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')]",
)
if self.wait_human_time:
time.sleep(1)
select_elem = self.get_element(
By.XPATH,
value="//select[contains(@class, 'o_add_custom_group_menu')]",
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}')]",
)
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}')]",
)
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')]",
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')]",
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')]",
)
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')]",
)
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(
"Cannot click, empty parameter from method"
" odoo_web_principal_menu_click_root_menu"
)
button = self.click_with_mouse_move(By.LINK_TEXT, from_text)
# If not work, force it
# selenium_tool.driver.execute_script("arguments[0].click();", button)
return button
def odoo_web_form_click_statusbar_button_status(
self, status_label, timeout=None
):
if timeout is None:
timeout = self.defaut_timeout
try:
status_button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'o_arrow_button') and"
f" contains(text(), '{status_label}')]",
timeout=timeout,
)
except Exception as e:
status_button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'o_arrow_button') and"
f" text()='{status_label}']",
timeout=timeout,
)
print(f"Bouton du statusbar avec le label '{status_label}' cliqué.")
return status_button
def odoo_profile_click(self, enable_dark_mode=False, enable_tour=False):
def toggle_data_menu(key, enable):
profile_button = self.click_with_mouse_move(
By.XPATH,
"//div[contains(@class, 'o_user_menu')]/button[contains(@class, 'py-1')]",
)
element = self.get_element(
By.XPATH,
f"//div[contains(@class, 'o_popover')]//span[@data-menu=\"{key}\"]//input",
)
if element.is_selected() != enable:
self.click_with_mouse_move(
By.XPATH,
f"//div[contains(@class, 'o_popover')]//span[@data-menu=\"{key}\"]//input",
)
else:
# close it
self.click_with_mouse_move(
By.XPATH,
"//div[contains(@class, 'o_user_menu')]/button[contains(@class, 'py-1')]",
)
toggle_data_menu("web_tour.tour_enabled", enable_tour)
toggle_data_menu("color_scheme.switch", enable_dark_mode)
def odoo_web_form_click_statusbar_button_status_floating(
self, status_label, timeout=None
):
if timeout is None:
timeout = self.defaut_timeout
status_button = self.click_with_mouse_move(
By.XPATH,
"//span[contains(@class, 'o_arrow_button') and contains(text(),"
f" '{status_label}')]",
timeout=timeout,
)
print(f"Bouton du statusbar avec le label '{status_label}' cliqué.")
return status_button
def odoo_web_form_click_statusbar_button_status_plus(
self, status_label, timeout=None
):
if timeout is None:
timeout = self.defaut_timeout
status_button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'o_arrow_button') and"
f" text()='{status_label}']",
timeout=timeout,
)
print(f"Bouton du statusbar avec le label '{status_label}' cliqué.")
return status_button
def odoo_web_form_click_button_statusbar(
self, btn_label, btn_class="btn-primary"
):
status_button = self.click_with_mouse_move(
By.XPATH,
f"//button[contains(@class, '{btn_class}') and contains(span,"
f" '{btn_label}')]",
)
print(f"Bouton du statusbar avec le label '{btn_label}' cliqué.")
return status_button
def odoo_web_form_click_button_action(self, btn_action):
status_button = self.click_with_mouse_move(
By.NAME,
btn_action,
)
print(f"Bouton avec l'action '{btn_action}' cliqué.")
return status_button
def odoo_web_form_click_save_action(self):
return self.odoo_web_form_click_by_classname_action(
"o_form_button_save"
)
def odoo_web_change_view(self, view_name="list", raise_error=False):
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")
def odoo_web_form_click_by_classname_action(self, custom_class_name):
status_button = self.click_with_mouse_move(
By.CLASS_NAME,
custom_class_name,
)
print(f"Bouton avec l'action '{custom_class_name}' cliqué.")
return status_button
def odoo_web_kanban_card(self, card_label):
# From web view kanban, list
kanban_card = self.click_with_mouse_move(
By.XPATH,
"//div[contains(@class,"
f" 'o_kanban_record')]//span[contains(text(), '{card_label}')]",
)
# kanban_card = self.driver.find_element(By.XPATH,
# f"//div[contains(@class, 'o_kanban_record')]//span[contains(text(), '{card_label}')]")
# kanban_card.click()
print(f"Carte Kanban avec le label '{card_label}' cliquée.")
return kanban_card
def odoo_web_action_button_new(self):
# From web view kanban, list
return self.click_with_mouse_move(By.CLASS_NAME, "o-kanban-button-new")
def odoo_web_kanban_click(self, button_text, btn_class="btn-primary"):
# From web view kanban, list
return self.click_with_mouse_move(
By.XPATH,
f"//button[contains(@class, 'btn {btn_class} o_kanban_edit') and"
f" text()='{button_text}']",
)
def odoo_web_form_notebook_tab_click(self, tab_text):
return self.click_with_mouse_move(
By.XPATH,
f"//a[text()='{tab_text}']",
)
# Web
def odoo_fill_widget_char(self, field_name, text, is_label=False):
# Type input
if self.odoo_version == "14.0":
text_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.NAME, field_name))
)
elif self.odoo_version == "16.0":
text_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, field_name))
)
text_field.send_keys(text)
return text_field
def odoo_fill_widget_selection(self, field_name, text):
return self.odoo_fill_widget_many2one(field_name, text)
def odoo_fill_widget_boolean(self, field_name, value):
self.driver.implicitly_wait(10)
checkbox_div = self.driver.find_element(
By.XPATH, f"//div[@name='{field_name}']"
)
checkbox = checkbox_div.find_element(
By.CSS_SELECTOR, 'input[type="checkbox"]'
)
is_selected = checkbox.get_attribute("selected")
# Check if already set
if is_selected == "true" and value:
return checkbox
elif is_selected is None and not value:
return checkbox
parent_element = checkbox.find_element(By.XPATH, "..")
parent_element.click()
return checkbox
def odoo_fill_widget_many2one(self, field_name, text):
# TODO read this file odoo/addons/web/static/src/js/fields/field_registry.js
div_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.NAME, field_name))
)
input_field = div_field.find_element(By.XPATH, ".//input")
input_field.send_keys(text)
# TODO improve speed by observation of comportement
time.sleep(1)
input_field.send_keys(Keys.ENTER)
return input_field
def odoo_fill_widget_date(self, field_name, date_to_select):
# Type date
self.driver.implicitly_wait(10)
checkbox_div = self.driver.find_element(
By.XPATH, f"//div[@name='{field_name}']"
)
checkbox_div.click()
# TODO improve speed by observation of comportement
time.sleep(0.5)
str_date_today = date_to_select.strftime("%Y-%m-%d")
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
def start_record(self):
# Démarrer l'enregistrement
if self.config.record_mode:
if self.config.record_wait_before_start_time:
time.sleep(int(self.config.record_wait_before_start_time))
# Sync before record
has_sync_error = False
if self.config.sync_file_record_read:
if not self.config.sync_file_record_write:
print(
"Error, cannot sync, missing 'sync_file_record_write'"
)
has_sync_error = True
if self.config.sync_file_record_write:
if not self.config.sync_file_record_read:
print(
"Error, cannot sync, missing 'sync_file_record_read'"
)
has_sync_error = True
if (
not has_sync_error
and self.config.sync_file_record_read
and self.config.sync_file_record_write
):
# Do sync
file_name_1 = f"/tmp/erplibre_sync_temp_file{self.config.sync_file_record_write}"
file_name_2 = f"/tmp/erplibre_sync_temp_file{self.config.sync_file_record_read}"
with open(file_name_1, "w") as file:
file.write(f"Recording {self.filename_recording}\n")
while not os.path.exists(file_name_2):
var_wait_time = 0.001
print(
f"File {self.filename_recording} wait"
f" {var_wait_time} seconds"
)
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)
# Arrêter l'enregistrement
if self.config.record_mode:
time.sleep(self.config.selenium_video_time_waiting_end)
self.video_recorder.stop()
print("End of recording video")
print(self.filename_recording)
filename_recording_mp4 = self.filename_recording.replace(
".webm", ".mp4"
)
if self.config.selenium_video_auto_convert_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}")
# By python-vlc
# media_player = vlc.MediaPlayer(filename)
# media = vlc.Media(filename)
# media_player.set_media(media)
# media_player.play()
# time.sleep(15)
# By external process
if self.config.selenium_video_auto_convert_mp4:
os.popen(f"vlc {filename_recording_mp4}")
else:
os.popen(f"vlc {self.filename_recording}")
def generer_code_postal_quebec(self):
# Zip code from quebec canada
premieres_lettres = ["G", "H", "J"]
premiere_lettre = random.choice(premieres_lettres)
deuxieme_lettre = chr(random.randint(65, 90))
chiffre = str(random.randint(0, 9))
troisieme_lettre = chr(random.randint(65, 90))
code_postal = f"{premiere_lettre}{chiffre}{deuxieme_lettre} {chiffre}{troisieme_lettre}{chiffre}"
return code_postal
def generer_numero_telephone_quebec(self):
# Number from quebec canada
indicateurs_regionaux = [
"418",
"438",
"450",
"514",
"579",
"581",
"819",
"873",
]
indicatif = random.choice(indicateurs_regionaux)
numero = "".join(random.choices("0123456789", k=7))
numero_telephone = f"({indicatif}) {numero[:3]}-{numero[3:]}"
return numero_telephone
def generer_date_naissance(self, age_minimum=16):
annee_actuelle = datetime.datetime.now().year
annee_maximale = annee_actuelle - age_minimum
annee_minimale = 1900
annee_naissance = random.randint(annee_minimale, annee_maximale)
mois_naissance = random.randint(1, 12)
jour_naissance = random.randint(1, 28)
date_naissance = datetime.datetime(
annee_naissance, mois_naissance, jour_naissance
)
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 wait_is_invisible(self, by, value):
# div_field = WebDriverWait(self.driver, 10).until(
# EC.presence_of_element_located((By.NAME, field_name))
# )
# wait = WebDriverWait(driver, 10)
# wait.until(
# EC.invisibility_of_element_located((By.ID, "ui-datepicker-div"))
# )
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 extraire_svg_graphique_by_3d(
self,
container_id: str = "svgbox627",
) -> Optional[str]:
"""
Récupère le code SVG du graphique D3 tel que rendu dans le DOM.
Le SVG est généré dynamiquement par D3.js après chargement de la page,
donc on récupère le HTML *après* exécution, pas la source initiale.
Returns:
Le code SVG complet (avec namespace et styles inline) ou None si absent.
"""
try:
# Récupérer le <svg> rendu via execute_script (outerHTML inclut les
# attributs et le contenu généré dynamiquement par D3).
svg_html = self.driver.execute_script(
"""
var container = document.getElementById(arguments[0]);
if (!container) return null;
var svg = container.querySelector('svg');
return svg ? svg.outerHTML : null;
""",
container_id,
)
if not svg_html:
return None
return self._svg_autonome(svg_html)
except Exception:
return None
def _svg_autonome(self, svg_html: str) -> str:
"""
Transforme un fragment SVG en document SVG autonome, viewable hors navigateur.
- Garantit le namespace xmlns
- Ajoute la déclaration XML
- Nettoie les attributs spécifiques à darkreader (extension de navigateur)
"""
# import re
# Nettoyer les attributs injectés par l'extension Dark Reader, qui polluent
# le SVG sans valeur ajoutée et peuvent contenir des --darkreader-* invalides.
svg_html = re.sub(r'\s*--darkreader-[^;"]*;?', "", svg_html)
svg_html = re.sub(r'\s*data-darkreader-[^=]*="[^"]*"', "", svg_html)
svg_html = re.sub(
r'<style class="darkreader[^"]*"[^>]*>.*?</style>',
"",
svg_html,
flags=re.DOTALL,
)
# S'assurer du namespace SVG (D3 le met normalement, mais on vérifie)
if 'xmlns="http://www.w3.org/2000/svg"' not in svg_html:
svg_html = svg_html.replace(
"<svg ",
'<svg xmlns="http://www.w3.org/2000/svg" ',
1,
)
# Préfixer avec la déclaration XML pour un fichier .svg standalone
return (
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'
+ svg_html
)
def convertir_svg_en_png_via_selenium(
self,
container_id: str,
chemin_png: str,
) -> bool:
"""
Alternative : capture le SVG via screenshot Selenium, sans cairosvg.
Avantage : pas de dépendance native.
Inconvénient : qualité dépend de la résolution du navigateur.
"""
try:
svg_element = self.get_element(
by=By.ID, value=container_id, timeout=5
)
except Exception as e:
return False
if not svg_element:
return False
# Scroller pour s'assurer que le SVG est visible
self.driver.execute_script(
"arguments[0].scrollIntoView({block: 'center'});", svg_element
)
time.sleep(0.3)
svg_element.screenshot(chemin_png)
return True
def telecharger_svg_graphique(
self,
chemin_sortie: str,
container_id: str = "svgbox627",
) -> bool:
"""
Télécharge le graphique en SVG dans un fichier.
Args:
driver: WebDriver positionné sur la page.
chemin_sortie: chemin du fichier .svg à créer.
container_id: id du conteneur du graphique (par défaut svgbox627).
Returns:
True si le fichier a été écrit, False si le graphique est absent.
"""
svg = self.extraire_svg_graphique_by_3d(container_id)
if not svg:
return False
os.makedirs(
os.path.dirname(os.path.abspath(chemin_sortie)) or ".",
exist_ok=True,
)
with open(chemin_sortie, "w", encoding="utf-8") as f:
f.write(svg)
return True
def get_init_list_file_unique(self):
# init_list_file = set(os.listdir(self.default_download_dir_path))
if self.config.use_network:
init_list_file = set(self.driver.get_downloadable_files())
else:
if os.path.isdir(self.default_download_dir_path):
init_list_file = set(
os.listdir(self.default_download_dir_path)
)
else:
init_list_file = set()
return init_list_file
def download_file_unique(
self, init_list_file, lst_unique_file, dct_document, filename
):
finish_download = False
fichiers = []
# 1 hour max
new_file_name = ""
max_time_to_wait = 60 * 60
i_wait = 0
while not finish_download:
if self.config.use_network:
fichiers = set(
self.driver.get_downloadable_files()
).difference(init_list_file)
else:
fichiers = set(
os.listdir(self.default_download_dir_path)
).difference(init_list_file)
diff_list_fichiers = list(fichiers.difference(lst_unique_file))
if diff_list_fichiers:
new_file_name = diff_list_fichiers[-1]
if not new_file_name.endswith(".part"):
# finish_download = True
break
i_wait += 1
if i_wait < max_time_to_wait:
time.sleep(1)
else:
return set()
lst_unique_file = fichiers
dct_document[filename] = os.path.join(
self.default_download_dir_path, new_file_name
)
print(len(fichiers))
print(fichiers)
print("--")
print(dct_document)
if self.config.use_network:
self.driver.download_file(
new_file_name, target_directory=self.default_download_dir_path
)
return lst_unique_file
def print_download_file_unique(self, dct_document, detect_error=False):
dct_output = {"document": dct_document, "has_error": detect_error}
print()
print("=DATA=")
print(json.dumps(dct_output))
print("=ENDDATA=")
print()
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(
"--url",
default="http://127.0.0.1:8069",
help="First URL to open.",
)
group_animation = parser.add_argument_group(title="Animation")
group_animation.add_argument(
"--selenium_default_delay",
default=0.4,
type=float,
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 address, example: http://localhost:4444",
)
group_browser.add_argument(
"--use_download_path_default",
action="store_true",
help="Actually, the download path is a temporary directory. "
"This will enable default path to /home/seluser/Downloads, "
"need this with Selenium Grid by network. "
"Will delete all file into /home/seluser/Downloads at startup.",
)
group_browser.add_argument(
"--window_size",
help="Example 1920,1080",
)
group_browser.add_argument(
"--headless",
action="store_true",
help="For automation without GUI.",
)
group_browser.add_argument(
"--no_dark_mode",
action="store_true",
help=(
"By default, will be in dark mode, because the main developer"
" like it!"
),
)
group_browser.add_argument(
"--firefox_binary_path",
help="Can specify firefox path to open selenium.",
)
group_browser.add_argument(
"--gecko_binary_path",
help="Can specify firefox path to open selenium.",
)
group_record = parser.add_argument_group(title="Recording")
group_record.add_argument(
"--record_mode",
action="store_true",
help="Start recording (not finish to be implemented).",
)
group_record.add_argument(
"--record_wait_before_start_time",
help="Time to wait in second before start recording.",
)
parser.add_argument(
"--video_suffix",
default="",
help="Will modify video name.",
)
parser.add_argument(
"--sync_file_record_read",
default="",
help=(
"Will sync and check file from tmp before record, write before"
" read."
),
)
parser.add_argument(
"--sync_file_record_write",
default="",
help=(
"Will sync and check file from tmp before record, write before"
" read."
),
)
group_record.add_argument(
"--selenium_video_time_waiting_end",
default=5,
type=int,
help="Pause at the end before close video.",
)
group_record.add_argument(
"--selenium_default_delay_presentation",
default=2,
type=float,
help="Pause at the end before close video.",
)
group_record.add_argument(
"--selenium_video_auto_play_video",
action="store_true",
help="Autoplay the video when done.",
)
group_record.add_argument(
"--selenium_video_auto_convert_mp4",
action="store_true",
default=True,
help="Will convert .webm to .mp4 when done.",
)