erplibre/script/selenium/selenium_lib.py

1206 lines
42 KiB
Python

#!/usr/bin/env python3
# © 2021-2024 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import datetime
import os
import random
import re
import sys
import time
from subprocess import getoutput
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.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
class SeleniumLib(object):
def __init__(self, config):
self.config = config
self.video_recorder = None
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
)
if not os.path.isdir(dir_path_screencast):
os.mkdir(dir_path_screencast)
self.driver = None
with open(".odoo-version") as txt:
self.odoo_version = txt.read()
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")
# 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_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:
self.driver = webdriver.Firefox(
options=firefox_options, service=firefox_services
)
except Exception:
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
)
# Ajout de l'enregistrement
if self.config.record_mode:
# Create recording
new_path = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..")
)
sys.path.append(new_path)
from script.selenium.selenium_video import VideoRecorder
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 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",
)
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])
@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=5):
wait = WebDriverWait(self.driver, timeout)
button = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
xpath,
)
)
)
button.click()
return button
def get_element(self, by: str = By.ID, value: str = None, timeout=5):
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.visibility_of_any_elements_located(
(
by,
value,
)
)
)
return ele[0]
def get_element_not_visible(
self, by: str = By.ID, value: str = None, timeout=5
):
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=5
):
wait = WebDriverWait(self.driver, timeout)
ele = wait.until(
EC.presence_of_all_elements_located(
(
by,
value,
)
)
)
return ele
def get_all_element(self, by: str = By.ID, value: str = None, timeout=5):
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=5,
inject_cursor=False,
):
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=5,
timeout_wait=None,
):
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=5
):
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=5
):
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=5
):
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 = 5,
no_scroll: bool = False,
viewport_ele_by: str = By.ID,
viewport_ele_value: str = None,
element: WebElement = None,
with_index: bool = False,
index_of_list: int = 0,
):
# ele = self.driver.find_element(by, value)
if element:
ele = element
else:
ele = self.get_element(by, value, timeout)
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).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:
button = wait.until(
EC.element_to_be_clickable(
(
by,
value,
)
)
)
try:
button.click()
except ElementClickInterceptedException as e:
try:
print(e)
self.driver.execute_script(
"arguments[0].scrollIntoView(true);", button
)
button.click()
except ElementClickInterceptedException as e:
print(e)
self.driver.execute_script("arguments[0].click();", button)
return button
def click_canvas_form(
self,
by: str = By.ID,
value: str = None,
timeout: int = 5,
form="carre",
):
# 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 = 5,
no_scroll: bool = False,
viewport_ele_by: str = By.ID,
viewport_ele_value: str = None,
clear_before: bool = False,
click_before: bool = False,
):
# 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,
offset_middle=True,
smooth_scroll=True,
smooth_speed=50,
smooth_fps=1 / 25,
viewport_ele=None,
):
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"]
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()
# 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):
self.driver.refresh()
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
):
# 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';
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';
// 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_text
+ """
// Ajouter le contenu SVG au div
overlay.innerHTML += svgContent;
// 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=30
)
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 == "16.0":
button = self.click_with_mouse_move(
By.XPATH,
"//button[contains(@class, 'dropdown-toggle') and"
" .//i[contains(@class, 'oi-apps')]]",
timeout=10,
)
elif self.odoo_version == "14.0":
button = self.click_with_mouse_move(
By.CSS_SELECTOR, "a.full[data-toggle='dropdown']", timeout=30
)
return button
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, timeout=30
)
# 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=10
):
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_web_form_click_statusbar_button_status_floating(
self, status_label, timeout=10
):
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=10
):
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}')]",
timeout=30,
)
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,
timeout=30,
)
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_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,
timeout=30,
)
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}')]",
timeout=30,
)
# 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", timeout=30
)
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}']",
timeout=30,
)
def odoo_web_form_notebook_tab_click(self, tab_text):
return self.click_with_mouse_move(
By.XPATH,
f"//a[text()='{tab_text}']",
timeout=30,
)
# 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)
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}']"
)
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 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:
os.popen(
"ffmpeg -i"
f" {self.filename_recording} {filename_recording_mp4}"
)
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 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_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(
"--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.",
)