ZetCode

测试主题切换器

最后修改日期: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 应用程序及其单个路由。

app.py
# 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。

templates/home.html
<!-- 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 文件用于样式化切换开关并定义浅色和深色主题。

static/style.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 开发者工具。

test/test_app.py
# 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 包(可选但推荐)

requirements.txt
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 开发者工具进行可视化调试。

作者

我的名字是 Jan Bodnar,我是一名充满热情的程序员,拥有多年丰富的经验。自 2007 年以来,我已撰写了超过 1400 篇编程文章和 8 本电子书。此外,我在教授编程概念方面也拥有八年以上的经验。

列出 所有 Python 教程