测试主题切换器
最后修改日期:2025 年 3 月 22 日
本教程将详细介绍如何构建一个带有主题切换功能的 Flask 应用程序,该应用程序将使用 Selenium 进行测试,并与 Chrome 开发者工具集成。该应用程序包含一个切换开关,用于在浅色和深色主题之间切换,Selenium 测试将验证 DOM 更改,并在执行过程中直观地显示开发者工具。
项目结构
下面将介绍应用程序的结构。
theme_app/
├── app.py # Flask app
├── static/
│ └── style.css # CSS for themes and toggle
├── templates/
│ └── home.html # Home page with toggle
└── test/
├── __init__.py # Makes test/ a package
└── test_app.py # Selenium tests with DevTools
设置 Flask 应用程序
该应用程序是一个简单的 Flask 应用,它提供一个带有主题切换开关的单一页面。该切换开关使用 JavaScript 在浅色和深色主题之间切换,通过向 `<body>` 元素应用 CSS 类来实现。Selenium 测试在单独的线程中启动应用程序,与切换开关进行交互,并使用 Chrome DevTools Protocol (CDP) 来检查 DOM 更改,打开开发者工具以进行视觉确认。
Flask 应用程序
此脚本定义了 Flask 应用程序及其单个路由。
# theme_app/app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html')
if __name__ == '__main__':
app.run(debug=True)
`app.py` 文件初始化了一个 Flask 应用程序,并定义了一个渲染 `home.html` 模板的单个路由 (`/`)。`app.run(debug=True)` 命令启动了调试模式下的开发服务器,以便在开发过程中获得实时反馈。
HTML 模板
此模板包含主题切换开关和用于主题切换的 JavaScript。
<!-- theme_app/templates/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Theme Switcher App</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>Welcome to Theme Switcher</h1>
<p>Toggle the switch below to change themes.</p>
<!-- Toggle Switch -->
<label class="switch">
<input type="checkbox" id="theme-toggle">
<span class="slider round"></span>
</label>
<span class="label-text">Light/Dark Mode</span>
</div>
<script>
const toggle = document.getElementById('theme-toggle');
const body = document.body;
// Load saved theme from localStorage
if (localStorage.getItem('theme') === 'dark') {
body.classList.add('dark-theme');
toggle.checked = true;
}
// Toggle theme on click
toggle.addEventListener('change', () => {
body.classList.toggle('dark-theme');
const theme = body.classList.contains('dark-theme') ? 'dark' : 'light';
localStorage.setItem('theme', theme);
});
</script>
</body>
</html>
`home.html` 模板创建了一个包含标题、段落和使用 `style.css` 中的 CSS 样式化的切换开关的页面。JavaScript 在加载时检查 `localStorage` 中保存的主题,如果设置为“dark”,则将 `dark-theme` 类应用于 `<body>`。单击切换开关会切换类并更新 `localStorage`,从而实现主题的持久化。
CSS
此 CSS 文件用于样式化切换开关并定义浅色和深色主题。
/* theme_app/static/style.css */
body {
font-family: Arial, sans-serif;
transition: background-color 0.3s, color 0.3s;
margin: 0;
padding: 0;
}
/* Light theme (default) */
body {
background-color: #f0f0f0;
color: #333;
}
/* Dark theme */
body.dark-theme {
background-color: #333;
color: #f0f0f0;
}
.container {
max-width: 800px;
margin: 50px auto;
text-align: center;
}
/* Toggle Switch Styles */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
vertical-align: middle;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #2196F3;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.label-text {
margin-left: 10px;
font-size: 16px;
}
`style.css` 文件定义了浅色主题(背景为 `#f0f0f0`)和深色主题(背景为 `#333`)的样式,并带有平滑过渡。它还样式化了切换开关,使用一个隐藏的复选框和一个滑动的 `.slider` 元素,该元素在选中时会改变颜色和位置。
Selenium 单元测试
此模块包含基于 Selenium 的单元测试,用于验证主题切换器,并在执行期间打开 Chrome 开发者工具。
# theme_app/test/test_app.py
import unittest
from flask import url_for
from app import app as flask_app
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.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
import threading
import time
from werkzeug.serving import make_server
class TestThemeSwitcher(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Configure Flask app
flask_app.config['TESTING'] = True
flask_app.config['SERVER_NAME'] = 'localhost:5001'
# Start Flask server in a separate thread with explicit stop capability
cls.server = make_server('localhost', 5001, flask_app, threaded=True)
cls.server_thread = threading.Thread(target=cls.server.serve_forever)
cls.server_thread.daemon = False # Non-daemon for explicit control
cls.server_thread.start()
# Wait for server to start
time.sleep(1)
# Set up Selenium WebDriver (using Chrome) with DevTools capabilities
options = webdriver.ChromeOptions()
# Ensure NOT headless so we can see DevTools
# options.add_argument('--headless') # Comment this out
options.add_argument('--auto-open-devtools-for-tabs') # Auto-open DevTools (optional)
cls.driver = webdriver.Chrome(options=options)
# Enable DevTools DOM domain
cls.driver.execute_cdp_cmd('DOM.enable', {})
@classmethod
def tearDownClass(cls):
# Disable DOM domain and clean up Selenium
cls.driver.execute_cdp_cmd('DOM.disable', {})
cls.driver.quit()
# Explicitly stop the Flask server
cls.server.shutdown()
cls.server_thread.join(timeout=5) # Wait for thread to finish, max 5s
if cls.server_thread.is_alive():
print("Warning: Server thread did not stop cleanly")
def setUp(self):
# Reset browser state before each test
self.driver.delete_all_cookies()
self.client = flask_app.test_client()
def test_home_page_basic(self):
"""Test the home page for status code and basic content"""
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertIn(b"<title>Theme Switcher App</title>", response.data)
self.assertIn(b'<input type="checkbox" id="theme-toggle">', response.data)
self.assertIn(b"body.classList.toggle('dark-theme')", response.data)
def test_theme_switching(self):
"""Test theme switching by inspecting DOM elements and showing DevTools"""
driver = self.driver
driver.get('https://:5001/')
# Wait for the label (which wraps the toggle) to be clickable
toggle_label = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CLASS_NAME, "switch"))
)
# Open Developer Tools using keyboard shortcut (Ctrl+Shift+I)
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).key_down(Keys.SHIFT).send_keys('i')\
.key_up(Keys.SHIFT).key_up(Keys.CONTROL).perform()
time.sleep(1) # Give DevTools a moment to open
# Get the document node ID for the root (HTML document)
document = driver.execute_cdp_cmd('DOM.getDocument', {})
root_node_id = document['root']['nodeId']
# Query the <body> element using DOM.querySelector
body_node = driver.execute_cdp_cmd('DOM.querySelector', {
'nodeId': root_node_id,
'selector': 'body'
})
body_node_id = body_node['nodeId']
# Get initial attributes of <body> (should not have dark-theme)
initial_attributes = driver.execute_cdp_cmd('DOM.getAttributes', {'nodeId': body_node_id})
initial_classes = initial_attributes.get('attributes', [])
class_index = initial_classes.index('class') + 1 if 'class' in initial_classes else -1
initial_class_value = initial_classes[class_index] if class_index >= 0 else ''
self.assertNotIn('dark-theme', initial_class_value)
# Check initial background color for confirmation
body = driver.find_element(By.TAG_NAME, 'body')
initial_bg = driver.execute_script(
"return window.getComputedStyle(arguments[0]).backgroundColor", body
)
self.assertEqual(initial_bg, 'rgb(240, 240, 240)') # #f0f0f0
# Click the label to toggle the theme (DevTools should show the change)
toggle_label.click()
# Wait for theme transition and let you see it in DevTools
time.sleep(2) # Increased to give you time to observe
# Get updated attributes of <body> (should now have dark-theme)
dark_attributes = driver.execute_cdp_cmd('DOM.getAttributes', {'nodeId': body_node_id})
dark_classes = dark_attributes.get('attributes', [])
dark_class_index = dark_classes.index('class') + 1 if 'class' in dark_classes else -1
dark_class_value = dark_classes[dark_class_index] if dark_class_index >= 0 else ''
self.assertIn('dark-theme', dark_class_value)
# Check dark theme background color
dark_bg = driver.execute_script(
"return window.getComputedStyle(arguments[0]).backgroundColor", body
)
self.assertEqual(dark_bg, 'rgb(51, 51, 51)') # #333
# Toggle back to light (DevTools should update)
toggle_label.click()
time.sleep(2) # Increased to give you time to observe
# Get final attributes of <body> (should not have dark-theme again)
light_attributes = driver.execute_cdp_cmd('DOM.getAttributes', {'nodeId': body_node_id})
light_classes = light_attributes.get('attributes', [])
light_class_index = light_classes.index('class') + 1 if 'class' in light_classes else -1
light_class_value = light_classes[light_class_index] if light_class_index >= 0 else ''
self.assertNotIn('dark-theme', light_class_value)
# Check light theme background color again
light_bg = driver.execute_script(
"return window.getComputedStyle(arguments[0]).backgroundColor", body
)
self.assertEqual(light_bg, 'rgb(240, 240, 240)')
# Optional: Keep browser open longer to inspect DevTools
time.sleep(3)
if __name__ == '__main__':
unittest.main()
`test_app.py` 模块使用 Selenium 测试 Flask 主题切换器应用程序。在 `setUpClass` 中,Flask 应用程序配置为在线程中使用 `werkzeug.serving.make_server` 在端口 5001 上运行,以实现显式的停止控制。使用可选的自动打开开发者工具的非无头 Chrome 实例进行初始化,并通过 CDP 启用 DOM 域。`tearDownClass` 会禁用 DOM 域,关闭浏览器,并通过 `shutdown` 和 `join` 显式停止服务器线程。
`test_home_page_basic` 测试使用 Flask 的测试客户端来验证页面的状态码和内容,检查标题、切换复选框和主题切换脚本。`test_theme_switching` 测试通过 `Ctrl+Shift+I` 打开开发者工具,检查初始的浅色主题(无 `dark-theme` 类,背景为 `#f0f0f0`),切换到深色(验证 `dark-theme` 类和背景为 `#333`),然后切换回浅色,并暂停以允许在开发者工具中进行检查。
要求
在 `requirements.txt` 文件中指定所需的 Python 包(可选但推荐)
flask selenium
使用以下命令安装依赖项
pip install -r requirements.txt
运行应用程序
要手动运行应用程序,请导航到 `theme_app` 目录并执行
flask run
在 `https://:5000` 访问应用程序以手动与主题切换器进行交互。
要运行测试,请导航到 `theme_app` 目录并执行
python -m unittest test.test_app -v
测试将启动 Flask 应用程序,打开带有开发者工具的 Chrome 浏览器,执行主题切换测试,并允许在关闭之前在“Elements”选项卡中直观地检查 DOM 更改。
在本文中,我们创建了一个带有主题切换器的 Flask 应用程序,并使用 Selenium 编写了单元测试来验证其功能,并集成了 Chrome 开发者工具进行可视化调试。
作者
列出 所有 Python 教程。