Labrecquev
English

labrecquev.ca

L'espace de Vincent sur le web


Automatisation du testing avec Python


Dans mon dernier emploi, j'ai été chargé de tester exhaustivement l'interface utilisateur d'un nouveau logiciel en développement et de participer à son développement.

Le directeur n'était pas au courant que je pouvais programmer, et donc s'attendait que ça me prenne des semaines à compléter la tâche manuellement à cause du grand nombre de produits invidivuels à vérifier.

Après quelques heures, j'ai réalisé que je pourrais écrire un programme qui ferait la vérification de chaque produit et prendrait les captures d'écran de chaque option et variante. Ça m'a pris une journée à programmer la première version opérationnelle, et ensuite quelques heures de mises à jour chaque fois que le logiciel était amélioré. Le programme prenait un peu moins d'une journée à passer tous les produis en revue. Je pouvais le partir le soir et il était prêt pour analyse le sur-lendemain. Le rapport servait pour la prochaine ronde d'améliorations à l'équipe de programmeurs.

Le programme a été programmé avec le package Selenium de Python. Voici un aperçu du code:

La première partie contient les déclarations d'importations des modules et les définitions des fonctions. Chaque action est définie dans une fonction et pourra ensuite être appelée au besoin dans le programme.
        import time
import pandas as pd
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.firefox.options import Options
from datetime import datetime

wt = 30 # wait time if element not available
def click_main_menu():
    WebDriverWait(driver, wt).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, 
                                    "div[id='vs-436c8801-3ba7-402f']"))).click()

def expand_main_menu():
    for i in range(2): # for some reason i need to click main menu twice in order for its class status to update
        click_main_menu()
        time.sleep(1)
        
    if check_if_main_menu_expanded() == False:
        click_main_menu()
    else:
        pass

def expand_font_menu():
    expand_main_menu()
    time.sleep(2)
    ul_ids = ['vs-78764df7-857b-44a9', 'vs-b4703944-3cd3-481b'] # sometimes another id is used
    for ul_id in ul_ids:
        try:        
            font_menu = driver.find_element_by_css_selector('ul[id='+ ul_id +']')
            driver.execute_script("arguments[0].click();", font_menu)
            break
        except:
            continue
    
def close_font_menu():
    WebDriverWait(driver, wt).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, 
                                    "span[id='vs-98b7a99a-a727-4bce']"))).click()
def expand_color_menu():
    expand_main_menu()
    for color_menu_id in ['vs-cd66235b-3468-49fd','vs-696a1a04-f8e3-42ba']:
        try:
            WebDriverWait(driver, 5).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, 
                                            "ul[id="+ color_menu_id +"]"))).click()
        except:
            continue
def close_color_menu():
    WebDriverWait(driver, wt).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 
                                        "span[id='vs-0b13c6ba-9318-4549']"))).click()
def close_image_menu():
    try:
        WebDriverWait(driver, 5).until(
                EC.presence_of_element_located((By.CSS_SELECTOR, 
                                            "span[id='vs-89db7149-7486-46e1']"))).click()
    except:
        pass
    
def toggle_to_image(): # deprecated
    WebDriverWait(driver, wt).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 
                                        "span[id='vs-0ecceb6d-e883-424e']"))).click()
def toggle_image(lbl_id):
    expand_main_menu()
    
    img_lbl = WebDriverWait(driver, wt).until(
            EC.presence_of_element_located((By.ID, lbl_id)))
    
    img_span_slider = img_lbl.find_element_by_css_selector('span[class="slider round"]')
    driver.execute_script("arguments[0].click();", img_span_slider) # solved "element not interactable problem
    
def toggle_name_and_dates(lbl_id):
    expand_main_menu()
    name_text_lbl = driver.find_element_by_id(lbl_id)
    name_text_slider = name_text_lbl.find_element_by_css_selector('span[class="slider round"]')
    driver.execute_script("arguments[0].click();", name_text_slider) # solved "element not interactable problem

def open_image_menu():
    expand_main_menu()
    span_ids = ['vs-59a37434-e24e-4cf9', 'vs-3fedd5bd-ccd6-45b3']
    for span_id in span_ids:
        try:
            WebDriverWait(driver, 5).until(
                EC.presence_of_element_located((By.CSS_SELECTOR, 
                                            "span[id="+ span_id +"]"))).click()
            time.sleep(2)
            break
        except:
            continue
    

def drag_and_screenshot(side, x, y, screenshot="no", go_back="no"):
    action = ActionChains(driver)
    action.click_and_hold(on_element=None)
    action.move_by_offset(x, y)
    action.release(on_element=None)
    action.perform()
    time.sleep(1)
    if screenshot == "yes":
        driver.save_screenshot(product_path + product[0] + "_" + side + ".png")
    if go_back == "yes":
        action = ActionChains(driver)
        action.click_and_hold(on_element=None)
        action.move_by_offset(-x, -y)
        action.release(on_element=None)
        action.perform()
        time.sleep(1)

def check_if_main_menu_expanded():
    menu = driver.find_element_by_css_selector("div[id='vs-436c8801-3ba7-402f']")
    menu_class = menu.get_attribute("class")
    if "collapsed" in menu_class:
        return False
    else:
        return True
    
def collapse_main_menu():
    for i in range(2): # for some reason i need to click main menu twice in order for its class status to update
        click_main_menu()
        time.sleep(1)
        
    if check_if_main_menu_expanded() == True:
        click_main_menu()
    else:
        pass
    
def get_surface_name():    
    surface_name = driver.find_element_by_id('vs-d85bfa6d-8df2-43b5')
    return surface_name.text
        
def toggle_surface(direction):
    if direction == "left":
        WebDriverWait(driver, wt).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 
                                        "span[id='vs-2b3a519e-2021-4c54']"))).click()    
    if direction == "right":
        WebDriverWait(driver, wt).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, 
                                        "span[id='vs-0ecceb6d-e883-424e']"))).click()
def get_surface_list():
    surface_list = [get_surface_name()]
    for direction in ['right', 'left']:
        for i in range(6):
            try:
                toggle_surface(direction)
                surface = get_surface_name()
                if surface not in surface_list:
                    surface_list.append(surface)
            except:
                pass
    surface_list = list(set(surface_list))
    return surface_list

def go_to_surface(surface):
    temp_surface = get_surface_name()        
    if surface == temp_surface:
        return None
    
    for direction in ['right', 'left']:
        try:
            for i in range(6):
                toggle_surface(direction)
                temp_surface = get_surface_name()
                if temp_surface == surface:
                    return None # exit loop
        except:
            pass
    time.sleep(1)
    
def move_camera_to_surface(surface):
    camera_dict = {"Front Surface":[0,0],"Top Surface": [0,150], "Back Surface": [330,0]}
    if surface in camera_dict.keys():
        x_y = camera_dict[surface]
        drag_and_screenshot(surface, x_y[0], x_y[1])   
        
def reinitialize_camera(surface):
    camera_dict = {"Front Surface":[-0,-0], "Top Surface": [-0,-150], "Back Surface": [-330,-0]}
    if surface in camera_dict.keys():
        x_y = camera_dict[surface]
        drag_and_screenshot(surface, x_y[0], x_y[1])
    
def capture_product_screenshots():
    try:
        collapse_main_menu()
        driver.save_screenshot(product_path + product[0] + "_front.png")
        drag_and_screenshot("left", 150, 0, screenshot="yes", go_back="yes")
        drag_and_screenshot("top", 0, 150, screenshot="yes", go_back="yes")
        drag_and_screenshot("back", 330, 0, screenshot="yes", go_back="yes")
        drag_and_screenshot("right", 480, 0, screenshot="yes", go_back="yes")
    except Exception as e: print(e)
    
def engraving_colors_validation():
    try:
        expand_color_menu()
        if not os.path.exists(colors_path): os.makedirs(colors_path)
        color_options = driver.find_element_by_css_selector("div[id='vs-eab5835d-e6c7-4ef0']")
        color_options = color_options.find_elements_by_tag_name('a')    
        for color in color_options: # try every color and take a screenshot     
            color_text = color.text
            # print(color_text)
            attributes.append((product[0],color_text, 1))
            color.click()
            wait("engraving_colors_validation")
            collapse_main_menu()
            driver.save_screenshot(colors_path + product[0] + "_" + color_text + ".png")
            expand_color_menu()
        close_color_menu()
        wait("engraving_colors_validation")
    except Exception as e: 
        print(e)
        attributes.append((product[0],"no_color_menu", 1))
            
def fonts_validation():
    try:        
        expand_font_menu()
        if not os.path.exists(fonts_path): os.makedirs(fonts_path)
        font_options = driver.find_element_by_css_selector("div[id='vs-0d5121ef-a302-4e1f']")
        font_options = font_options.find_elements_by_tag_name('a')
        font_index = 0
        
        for font in font_options:
            # wait_time = 20 if font_index > 0 else 5
            font_text = font.text
            # print(font_text)
            attributes.append((product[0],font_text, 1))
            font.click()
            # time.sleep(wait_time)
            wait("fonts_validation")
            collapse_main_menu()
            driver.save_screenshot(fonts_path + product[0] + "_" + font_text + ".png")
            expand_font_menu()
            font_index += 1
        close_font_menu()
        # time.sleep(10)
        wait("fonts_validation")
    except Exception as e: 
        print(e)
        attributes.append((product[0],"no_font_menu", 1))
    
def images_validation():
    try:
        if not os.path.exists(images_path): os.makedirs(images_path)
        open_image_menu()
        
        # get list of all images in menu and screenshot each 
        image_divs = driver.find_elements_by_css_selector("div[id*='vs-462b9c22-3c3e-4184']")
        meca = 0
        laser = 0
    
        for image_div in image_divs: # get image code for each image and append to list
            image_name = image_div.get_attribute('id')
            image_name = image_name.split('_')
            image_name = image_name[-1]
            product_image_list.append((product[0], image_name))
            if "M" in image_name: # detect if there were images with M
                meca = 1
            else:
                laser = 1
                
            def screenshot_image():
                image_div.click()
                # time.sleep(25)
                wait("images_validation")
                collapse_main_menu()
                driver.save_screenshot(images_path + product[0] + "_" + image_name + ".png")
                open_image_menu()
            # if product_index % 10 == 0:
            #     screenshot_image()
            #     pass
        
        if meca == 1:
            attributes.append((product[0],"meca", 1))
        if laser == 1:
            attributes.append((product[0],"laser", 1))
        close_image_menu()
        # time.sleep(10)
        wait("images_validation")
    except Exception as e: print(e)
        
def enable_image_and_text(surface):    
    d = {"Top Surface":top_lbl_ids, "Back Surface":back_lbl_ids, "Front Surface":front_lbl_ids}
    
    try:
        print("toggling image")
        toggle_image(d[surface]['image'])
        # time.sleep(35)
        wait("enable_image_and_text")
    except Exception as e: print(e)
    
    try:
        print("toggling text")
        toggle_name_and_dates(d[surface]['text'])
        # time.sleep(35)
        wait("enable_image_and_text")
    except Exception as e: print(e)

def get_splashScreen_style():
    splashScreen = driver.find_element_by_id('splashScreen')
    splashScreen_style = splashScreen.get_attribute('style')
    return splashScreen_style

def wait(func_name):
    try:
        time.sleep(3)
        splashScreen_style = get_splashScreen_style()
        secs = 5
        while splashScreen_style == "":
            time.sleep(1)
            secs += 1
            if secs > 45 : 
                print("splashScreen > 50secs, break during func:", func_name)
                break
            splashScreen_style = get_splashScreen_style()
        time.sleep(2)
    except Exception as e: print(e)
        
def enable_image(surface):
    try:
        if surface == "Top Surface":
            toggle_image(top_lbl_ids['image'])
            # time.sleep(25)     
            wait("enable_image")
        if surface == "Back Surface":
            toggle_image(back_lbl_ids['image'])
            # time.sleep(25)     
            wait("enable_image")
        if surface == "Front Surface":
            toggle_image(front_lbl_ids['image'])
            # time.sleep(25)     
            wait("enable_image")
    except Exception as e: 
        print(e)
        attributes.append((product[0],"no_image_menu", 1))
        
def check_text_fields():
    try:
        expand_main_menu()
        for name_input_id in ['vs-da61663f-cda3-47ca', 'vs-29217229-5747-4857']:
            try:            
                name_input = driver.find_element_by_id(name_input_id)
                name_input.clear()
                name_input.send_keys("Jean-Roch Martel de la Rochelière (44 chars)")
            except:
                continue
        
        for mid_input_id in ['vs-41c7e01e-c6e9-4ad1','vs-30b26d11-89a8-402d']:
            try:
                mid_input = driver.find_element_by_id(mid_input_id)
                mid_input.clear()
                mid_input.send_keys("Un père dévoué à sa famille et ses proches (53 chars)")
            except:
                continue
        
        for date_input_id in ['vs-6daa182a-6ab2-4db6','vs-58d18d04-34a5-4ace']:
            try:
                date_input = driver.find_element_by_id(date_input_id)
                date_input.clear()
                date_input.send_keys("1910 - 2010")
            except:
                continue
        #submit
        for submit_id in ['vs-65d999ee-c788-4889','vs-f2b30205-15b9-4051']:
            try:
                WebDriverWait(driver, 5).until(
                        EC.presence_of_element_located((By.CSS_SELECTOR, 
                                                    "a[id=" + submit_id + "]"))).click()
            except:
                continue
        # time.sleep(45)
        wait("check_text_fields")
        
    except Exception as e: print(e)
        
    
La deuxième partie contient le programme de testing. Il prend la liste des URLs à tester, et exécute la routine pour chaque produit.
        path = ".../Configurateur 3D/testing/"

df = pd.read_excel(path + "master_testing.xlsx",sheet_name="ronde 5",skiprows=3)

n_df = df[df['Repasser le robot'] == 1]

product_list = list(zip(n_df['code'], n_df['url']))

report_save_path = ".../Documents/temp_drive/"
base_url = "..."

attributes = []
product_image_list = []
product_index = 0

# products_done = [x for x in os.listdir(report_save_path) if ".xlsx" not in x] # products for which I have images

for product in product_list:
    product_start = datetime.now()
    url = product[1]
    print(product[0], url)
    attributes.append((product[0], "url", url))
    options = Options()
    options.binary_location = r'.../Mozilla Firefox/firefox.exe'
    driver = webdriver.Firefox(options=options)
    driver.set_window_size(1920, 1080) # for better screenshots
    driver.get(url) # open product page
    
    # time.sleep(10) # wait for page loading
    wait("initial wait")
    
    # find div id splashScreen where style="display: none;"
    try:
        splashScreen = driver.find_element_by_id('splashScreen')
        splashScreen_style = splashScreen.get_attribute('style')
    except:
        print("could not get splashscreen, next product")
        driver.close()
        continue
    
    if splashScreen_style == "": # page produit n'ouvre pas
        attributes.append((product[0],"produit ouvre", 0))
        print("produit n'ouvre pas")
        driver.close()
        continue
    
    attributes.append((product[0],"produit ouvre", 1))
    print("produit ouvre")
    
    # paths
    product_path = report_save_path + product[0] + "/"
    colors_path = product_path + "couleurs gravure/"
    fonts_path = product_path + "polices/"
    images_path = product_path + "images/"
    
    try: # check presence of menu
        menu = driver.find_element_by_css_selector("div[id='vs-436c8801-3ba7-402f']")
        attributes.append((product[0],"menu_disponible", 1))
    except:
        attributes.append((product[0],"menu_disponible", 0))
        print("menu unavailable")
        driver.close()
        continue
    
    # create product path
    if not os.path.exists(product_path): os.makedirs(product_path)
        
    # surfaces    
    surface_list = get_surface_list()
    main_surface = "Front Surface" if len(surface_list) > 1 else surface_list[0]
    front_lbl_ids = {'image':'vs-9a1e0552-7792-4055', 'text': 'vs-fd9f96da-6671-4f46'}
    top_lbl_ids = {'image':'vs-72cb0d23-f728-4f10', 'text': 'vs-57d24b11-600c-4a43'}
    back_lbl_ids = {'image':'vs-c3aeabee-02e6-40b0', 'text': 'vs-92fea04e-a6f9-4846'}
    
    
    for surface in surface_list:
        go_to_surface(surface)
        
        print("current surface:", surface)
        move_camera_to_surface(surface)
        
        if surface == main_surface:
            print("main surface, enable_image")
            enable_image(surface)            
            
            print("engraving_colors_validation")
            engraving_colors_validation()
            
            print("fonts_validation")
            fonts_validation()
            
            print("images_validation")
            images_validation()

            print("check_text_fields")
            check_text_fields()
            
        else:                    
            print("not main surface, enable_image_and_text")
            enable_image_and_text(surface)
            
        reinitialize_camera(surface)
        
    print("capture_product_screenshots")
    capture_product_screenshots()
    
    driver.close()
    product_index += 1
    print("product done, it took:", datetime.now() - product_start)
        
    
La dernière partie est la génération du rapport. Pour chaque produit testé, des données étaient collectées par le robot sur la présence d'options et d'éléments. Un rapport Excel était généré automatiquement.
        def report():
    now = datetime.now().strftime('%d-%m-%Y %H%M%S')
    # img_df = pd.DataFrame(product_image_list, columns=['code', 'image'])
    # img_df.to_excel(report_save_path + 'product_images'+ now +'.xlsx') # save product-image list in Excel
    
    attr_df = pd.DataFrame(attributes, columns=['code', 'attr', 'value'])
    pivot = attr_df.pivot(index="code", columns="attr", values="value")
    pivot.to_excel(report_save_path + 'data'+ now +'.xlsx') # save product attributes in Excel
report()