PlusMagi's Blog By Pitt Phunsanit

Selenium: Mini Framework

ถ้าเขียน selenium จะเห็นว่ามันจะมีส่วนที่เหมือนเขียน code อื่น ๆ (จริง ๆ มันก็คือ code) นั่นละ ที่จะมีส่วนที่ทำซ้ำ ๆ ในทุก project เลยจะ dry ส่วนซ้ำ ๆ ออกมาเป็น Mini Framework


ส่วนแรกคือ selenium_test.py เป็นส่วนที่เก็บ code ส่วนที่ใช้ซ้ำบ่อย ๆ อย่าง สร้าง folder เก็บ report, screenshot
selenium_test.py

import unittest
import time
import os
import subprocess
import threading
import http.server
import socketserver
import sys
from datetime import datetime
from selenium_base_test import BaseSQLMappingAppTest # --- Configuration ---
SOURCE_CODE_DIR = os.path.abspath (os.path.join (os.path.dirname (__file__) , 'SourceCode')) DIST_DIR = os.path.join (SOURCE_CODE_DIR, 'dist') SERVER_PORT = 8000
SELENIUM_TEST_DIR = os.path.join (os.path.dirname (__file__) , "selenium") MAIN_SCREENSHOT_DIR = os.path.join (os.path.dirname (__file__) , "temp/selenium", datetime.now () .strftime ("%Y%m%d_%H%M%S")) # --- Global Variables ---
httpd_server = None def start_server () : global httpd_server class Handler (http.server.SimpleHTTPRequestHandler) : def __init__ (self, *args, **kwargs) : super () .__init__ (*args, directory=DIST_DIR, **kwargs) httpd_server = socketserver.TCPServer ( ("", SERVER_PORT) , Handler) print (f"\nStarting local server for '{DIST_DIR}' at http://localhost:{SERVER_PORT}") httpd_server.serve_forever () class ScreenshotTestResult (unittest.TextTestResult) : def __init__ (self, stream, descriptions, verbosity, screenshot_dir=None) : super () .__init__ (stream, descriptions, verbosity) self.screenshot_dir = screenshot_dir def addError (self, test, err) : super () .addError (test, err) # Set a flag on the test instance to prevent success screenshot setattr (test, '_test_has_failed', True) driver = getattr (test, 'driver', None) # This block now runs ALWAYS on error, regardless of --log-all if driver and self.screenshot_dir: test_method_name = test.id () .split ('.') [-1] print (f"\n--- Test '{test_method_name}' FAILED. Performing error handling. ---") screenshot_path = os.path.join (self.screenshot_dir, f"{test_method_name}_ERROR.png") driver.save_screenshot (screenshot_path) print (f"--- Saved error screenshot to: {screenshot_path} ---") # FIX: Change browser log filename on error browser_log_path = os.path.join (self.screenshot_dir, f"{test_method_name}_browser_ERROR.log") try: browser_logs = driver.get_log ('browser') with open (browser_log_path, 'w') as f: f.write (f"==== Browser Console Logs for {test_method_name} ====\n") if browser_logs: for entry in browser_logs: f.write (f"[{entry['level']}] {entry['message']}\n") else: f.write ("No browser console logs were found.\n") print (f"--- Browser console logs saved to: {browser_log_path} ---") except Exception as e: with open (browser_log_path, 'w') as f: f.write (f"Could not retrieve browser logs: {e}\n") print (f"--- Error retrieving browser logs. See: {browser_log_path} ---") print (f"--- BROWSER FOR '{test_method_name}' WILL REMAIN OPEN for inspection. ---") setattr (test, 'keep_browser_open', True) class CustomTestRunner (unittest.TextTestRunner) : def __init__ (self, *args, **kwargs) : self.screenshot_dir = kwargs.pop ('screenshot_dir', None) super () .__init__ (*args, **kwargs) def _makeResult (self) : return ScreenshotTestResult (self.stream, self.descriptions, self.verbosity, self.screenshot_dir) def run_tests () : server_thread = threading.Thread (target=start_server) server_thread.daemon = True server_thread.start () time.sleep (1) try: BaseSQLMappingAppTest.SCREENSHOT_DIR = MAIN_SCREENSHOT_DIR if '--log-all' in sys.argv: BaseSQLMappingAppTest.LOG_ALL = True sys.argv.remove ('--log-all') print ("--- Log-all mode enabled. A screenshot will be taken for every test. ---") print (f"\n--- Discovering tests in: {SELENIUM_TEST_DIR} ---") loader = unittest.TestLoader () suite = loader.discover (start_dir=SELENIUM_TEST_DIR, pattern="test_*.py") log_file_path = os.path.join (MAIN_SCREENSHOT_DIR, 'test.log') print (f"--- Starting test run. Full log will be saved to: {log_file_path} ---") with open (log_file_path, 'w') as log_file: runner = CustomTestRunner (stream=log_file, verbosity=2, screenshot_dir=MAIN_SCREENSHOT_DIR) result = runner.run (suite) print (f"--- Test run complete. Full log saved to: {log_file_path} ---") print (f"--- All artifacts are in: {os.path.abspath (MAIN_SCREENSHOT_DIR) } ---") if not result.wasSuccessful () : sys.exit (1) finally: global httpd_server if httpd_server: print ("\n--- Shutting down local server ---") httpd_server.shutdown () httpd_server.server_close () if __name__ == "__main__": os.makedirs (MAIN_SCREENSHOT_DIR, exist_ok=True) run_tests () 

ส่วนที่เก็บ config ที่ใช้เฉพาะ project อย่าง run server, login
selenium_base_test.py

import unittest
import time
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import TimeoutException # --- Configuration (can be accessed by all tests) ---
APP_URL = "http://localhost:8000" class BaseSQLMappingAppTest (unittest.TestCase) : """ This is the base class for all test cases. It contains the common setup, teardown, and helper methods. """ LOG_ALL = False SCREENSHOT_DIR = "" # This will be set by the test runner _test_has_failed = False # Internal flag to track test failure def setUp (self) : self._test_has_failed = False self.keep_browser_open = False chrome_options = Options () chrome_options.set_capability ('goog:loggingPrefs', {'browser': 'ALL'}) self.driver = webdriver.Chrome (options=chrome_options) self.driver.get (APP_URL) # FIX: Increased timeout to 30 seconds self.wait = WebDriverWait (self.driver, 30) def tearDown (self) : # FIX: This is the new, correct logic for --log-all. # If log-all is enabled AND the test has NOT failed, take a success screenshot. # This block will be SKIPPED if the test failed, preserving the _ERROR.png file. if self.LOG_ALL and not self._test_has_failed: test_method_name = self.id () .split ('.') [-1] screenshot_path = os.path.join (self.SCREENSHOT_DIR, f"{test_method_name}_SUCCESS.png") self.driver.save_screenshot (screenshot_path) print (f"--- Saved success screenshot for '{test_method_name}' to: {screenshot_path} ---") if not self.keep_browser_open: self.driver.quit () def wait_for_editor (self, editor_name) : try: container_id = "SQLSourceIDE" if editor_name == "sqlSourceEditor" else "SQLOutputIDE" self.wait.until (EC.presence_of_element_located ( (By.ID, container_id))) self.wait.until (lambda driver: driver.execute_script (f"return window.monacoEditors && window.monacoEditors.{editor_name}")) except TimeoutException: print (f"Timeout waiting for Monaco Editor instance '{editor_name}' to be created.") raise def get_monaco_editor_text (self, editor_name) : self.wait_for_editor (editor_name) js_script = f"return window.monacoEditors.{editor_name}.getValue () ;" return self.driver.execute_script (js_script) def set_monaco_editor_text (self, editor_name, text) : self.wait_for_editor (editor_name) escaped_text = text.replace ('\\', '\\\\') .replace ("'", "\\'") .replace ('"', '\\"') .replace ('\n', '\\n') .replace ('`', '\\`') js_script = f"window.monacoEditors.{editor_name}.setValue (`{escaped_text}`) ;" self.driver.execute_script (js_script) 

ส่วนสุดท้าย แต่ไม่ท้ายสุดคือ Test Case แยกออกมาเป็นไฟล์เล็ก ๆ ให้สามารถ copy ไปวาง reused ได้ง่าย ๆ จะเก็บใน /Selenium หลาย ๆ ไฟล์ เช่น
test_01_initial_load

from selenium_base_test import BaseSQLMappingAppTest class TestInitialLoad (BaseSQLMappingAppTest) : def test_01_initial_load_and_default_sql (self) : print ("\nRunning test_01_initial_load_and_default_sql...") editor_text = self.get_monaco_editor_text ("sqlSourceEditor") self.assertIn ("INSERT INTO Customers", editor_text) print ("OK") 

เพราะการแยกเป็นส่วน ๆ เหมือนการทำ modular ทำให้ reused ได้ง่ายขึ้น

Exit mobile version