Labrecquev
Français

labrecquev.ca

Vincent's home on the web


Automating User Interface (UI) testing using Python


In my last job I was tasked to test extensively the User Interface (UI) of a new software under development and participate in the continuous development of the software.

The director did not know I was able to program, and so expected the task to take weeks to accomplish manually, due to the sheer amount of individual products to check.

After the first few hours, I worked out that I could write a program that would check every product and take screenshots of every option. It took me a day to write the first operational version, and then a few hours every time the software was modified. It took a bit less than 24 hours to verify all the products. I would have a report ready two days after and it would be used by the programmers team to make the next improvements.

It was written in Python using Selenium package. Here's an overview of the code:

The first part contains the modules imports and the functions definitions. Every action is defined in a function and can be called at will.
        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)
        
    
The second part is the actual testing program. It gets the list of all URLs to test, and executes a routine to perform for each product.
        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)
        
    
The last part is the reporting. For every product, data was collected regarding the presence of elements, options, and so on. An excel report was automatically generated.
        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()