测试响应式布局
最后修改于 2025 年 3 月 23 日
本教程展示了如何使用 Flask 和 CSS Grid 创建响应式圣杯布局,然后使用 Selenium 和 unittest 对布局和可见性进行测试。
简介
Flask 是一个用于构建 Web 应用程序的 Python 微框架。本指南使用 CSS Grid 演示了圣杯布局——一种经典的网页设计,包含页眉、页脚、两个侧边栏和主内容。我们将测试其在桌面、平板电脑和移动设备视口上的响应能力。
项目结构
holy_grail_app/
├── app.py # Flask app
├── static/
│ └── style.css # CSS with Grid
├── templates/
│ └── home.html # HTML layout
└── test/
└── test_layout.py # Selenium test
Flask 应用
Flask 应用提供了一个具有圣杯布局的单个页面。
# holy_grail_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, port=5001)
此脚本作为 Flask Web 应用程序的入口点。它初始化 Flask 实例 app,并定义一个单一路由 '/',该路由在被访问时渲染 home.html 模板。当直接执行时,应用程序以调试模式在端口 5001 上运行,提供增强的开发和调试功能。此基础设置允许轻松扩展和进一步自定义 Web 应用程序。
HTML 模板
HTML 定义了布局结构。将其另存为 templates/home.html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Holy Grail Layout</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="grid-container">
<header class="header">Header</header>
<aside class="left-sidebar">Left Sidebar</aside>
<main class="main-content">Main Content</main>
<aside class="right-sidebar">Right Sidebar</aside>
<footer class="footer">Footer</footer>
</div>
</body>
</html>
此 HTML 结构使用语义元素和响应式网格布局来实现网页设计。主布局位于类名为 grid-container 的 <div> 元素内,包含 <header>(用于标题或导航)、两个 <aside> 元素(用于左侧和右侧侧边栏)、<main>(用于中心内容)和 <footer>(用于页脚详细信息)等部分。每个部分都通过类名进行了语义和结构上的定义,确保了清晰度、可访问性和与 CSS 样式的兼容性。
带 Grid 的 CSS
CSS Grid 创建了响应式布局。将其另存为 static/style.css。
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
}
.grid-container {
display: grid;
grid-template-areas:
"header header header"
"left-sidebar main-content right-sidebar"
"footer footer footer";
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header {
grid-area: header;
background-color: #f1c40f;
padding: 20px;
text-align: center;
}
.left-sidebar {
grid-area: left-sidebar;
background-color: #2ecc71;
padding: 20px;
}
.main-content {
grid-area: main-content;
background-color: #ecf0f1;
padding: 20px;
}
.right-sidebar {
grid-area: right-sidebar;
background-color: #3498db;
padding: 20px;
}
.footer {
grid-area: footer;
background-color: #e74c3c;
padding: 20px;
text-align: center;
}
@media (max-width: 1024px) {
.grid-container {
grid-template-areas:
"header header"
"main-content main-content"
"left-sidebar right-sidebar"
"footer footer";
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr auto auto;
}
}
@media (max-width: 768px) {
.grid-container {
grid-template-areas:
"header"
"main-content"
"left-sidebar"
"right-sidebar"
"footer";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto auto auto;
}
}
此 CSS 定义了一个响应式且结构化的网格布局,确保了干净且视觉吸引人的设计。通用的 box-sizing 规则通过包含内边距和边框来简化元素尺寸计算。body 样式消除了默认边距,并应用了现代字体以获得精致的外观。.grid-container 将内容组织到命名的网格区域:页眉、页脚、侧边栏和主内容,并具有特定的行和列。每个部分都通过独特的背景颜色和内边距进行样式设计,从而创建清晰的视觉分隔。布局跨越了整个视口高度(min-height: 100vh),确保无论内容多少都能覆盖整个页面。
Selenium 测试
测试跨视口验证布局和可见性。将其另存为 test/test_layout.py。
# holy_grail_app/test/test_layout.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
import threading
import time
from werkzeug.serving import make_server
class TestLayoutResponsiveness(unittest.TestCase):
@classmethod
def setUpClass(cls):
flask_app.config['TESTING'] = True
flask_app.config['SERVER_NAME'] = 'localhost:5001'
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
cls.server_thread.start()
time.sleep(1)
options = webdriver.ChromeOptions()
cls.driver = webdriver.Chrome(options=options)
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
cls.server_thread.join(timeout=5)
if cls.server_thread.is_alive():
print("Warning: Server thread did not stop cleanly")
cls.driver.quit()
def setUp(self):
self.driver.delete_all_cookies()
def get_computed_style(self, selector, property_name):
script = f"""
const elem = document.querySelector('{selector}');
return window.getComputedStyle(elem).getPropertyValue('{property_name}');
"""
return self.driver.execute_script(script)
def get_bounding_rect(self, selector):
script = f"""
const elem = document.querySelector('{selector}');
const rect = elem.getBoundingClientRect();
return {{
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height
}};
"""
return self.driver.execute_script(script)
def get_viewport_width(self):
return self.driver.execute_script("return window.innerWidth;")
def test_layout_responsiveness_and_visibility(self):
driver = self.driver
driver.get('https://:5001/')
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "grid-container"))
)
viewports = [
{"name": "Desktop", "width": 1280, "height": 800},
{"name": "Tablet", "width": 800, "height": 600},
{"name": "Mobile", "width": 400, "height": 600},
]
elements = [
{"selector": ".header", "name": "Header"},
{"selector": ".left-sidebar", "name": "Left Sidebar"},
{"selector": ".main-content", "name": "Main Content"},
{"selector": ".right-sidebar", "name": "Right Sidebar"},
{"selector": ".footer", "name": "Footer"},
]
for vp in viewports:
with self.subTest(viewport=vp["name"]):
driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {
'width': vp["width"],
'height': vp["height"],
'deviceScaleFactor': 1,
'mobile': vp["width"] <= 400
})
time.sleep(1)
viewport_width = self.get_viewport_width()
container_width = self.get_computed_style('.grid-container', 'width')
header_width = self.get_computed_style('.header', 'width')
left_width = self.get_computed_style('.left-sidebar', 'width')
main_width = self.get_computed_style('.main-content', 'width')
right_width = self.get_computed_style('.right-sidebar', 'width')
footer_width = self.get_computed_style('.footer', 'width')
header_grid = self.get_computed_style('.header', 'grid-area')
left_grid = self.get_computed_style('.left-sidebar', 'grid-area')
main_grid = self.get_computed_style('.main-content', 'grid-area')
right_grid = self.get_computed_style('.right-sidebar', 'grid-area')
footer_grid = self.get_computed_style('.footer', 'grid-area')
if vp["name"] == "Desktop":
self.assertEqual(header_grid, 'header')
self.assertEqual(left_grid, 'left-sidebar')
self.assertEqual(main_grid, 'main-content')
self.assertEqual(right_grid, 'right-sidebar')
self.assertEqual(footer_grid, 'footer')
self.assertEqual(float(left_width[:-2]), 200)
self.assertEqual(float(right_width[:-2]), 200)
self.assertEqual(float(main_width[:-2]), viewport_width - 400)
self.assertEqual(float(header_width[:-2]), viewport_width)
self.assertEqual(float(footer_width[:-2]), viewport_width)
elif vp["name"] == "Tablet":
self.assertEqual(header_grid, 'header')
self.assertEqual(main_grid, 'main-content')
self.assertEqual(left_grid, 'left-sidebar')
self.assertEqual(right_grid, 'right-sidebar')
self.assertEqual(footer_grid, 'footer')
expected_half = viewport_width / 2
self.assertAlmostEqual(float(left_width[:-2]), expected_half, delta=10)
self.assertAlmostEqual(float(right_width[:-2]), expected_half, delta=10)
self.assertEqual(float(main_width[:-2]), viewport_width)
self.assertEqual(float(header_width[:-2]), viewport_width)
self.assertEqual(float(footer_width[:-2]), viewport_width)
elif vp["name"] == "Mobile":
self.assertEqual(header_grid, 'header')
self.assertEqual(main_grid, 'main-content')
self.assertEqual(left_grid, 'left-sidebar')
self.assertEqual(right_grid, 'right-sidebar')
self.assertEqual(footer_grid, 'footer')
self.assertEqual(float(header_width[:-2]), viewport_width)
self.assertEqual(float(main_width[:-2]), viewport_width)
self.assertEqual(float(left_width[:-2]), viewport_width)
self.assertEqual(float(right_width[:-2]), viewport_width)
self.assertEqual(float(footer_width[:-2]), viewport_width)
for elem in elements:
display = self.get_computed_style(elem["selector"], 'display')
visibility = self.get_computed_style(elem["selector"], 'visibility')
self.assertNotEqual(display, 'none')
self.assertNotEqual(visibility, 'hidden')
rect = self.get_bounding_rect(elem["selector"])
self.assertGreater(rect['width'], 0)
self.assertGreater(rect['height'], 0)
self.assertGreaterEqual(rect['top'], 0)
self.assertLess(rect['top'] + rect['height'], vp['height'] + 100)
self.assertGreaterEqual(rect['left'], 0)
self.assertLess(rect['left'] + rect['width'], vp['width'] + 20)
print(f"{vp['name']}: Container width={container_width}, "
f"Header={header_width}, Left={left_width}, "
f"Main={main_width}, Right={right_width}, Footer={footer_width}")
if __name__ == '__main__':
unittest.main()
此 Python 测试文件使用 unittest 和 Selenium 来验证 Web 应用程序布局的响应性和可见性。它设置了一个配置在 localhost:5001 上的 Flask 测试服务器,并初始化了一个 Selenium WebDriver 用于自动化浏览器测试。setUpClass 和 tearDownClass 方法负责服务器和 WebDriver 的生命周期管理,而 setUp 方法通过在每次测试前清除 cookie 来确保干净的浏览器状态。
主要关注点是 test_layout_responsiveness_and_visibility 方法,该方法跨多个视口验证布局:桌面、平板电脑和移动设备。使用 Selenium,它动态调整浏览器尺寸并检查 CSS 属性,如 grid-area、width 和 display,以确保所有元素都正确放置并可见。自定义辅助方法,如 get_computed_style 和 get_bounding_rect,用于检索和验证元素样式和尺寸。此综合测试确保布局在各种屏幕尺寸上都能正确适应,同时保持结构和功能。
运行应用程序和测试
安装依赖项,然后按以下方式运行应用程序和测试。
$ pip install flask selenium $ python holy_grail_app/app.py
为了进行测试,请确保 ChromeDriver 在您的 PATH 中,然后运行
$ cd holy_grail_app $ python -m unittest test.test_layout -v
在本文中,我们重点关注了 Flask 应用程序响应式布局的测试。设置包括一个使用 unittest 和 Selenium 进行浏览器自动化的 Python 测试文件。初始化并本地运行了一个 Flask 测试服务器,作为跨多种设备进行布局验证的基础。通过详细的测试,使用对应于桌面、平板电脑和移动设备环境的视口,验证了布局的适应性。
作者
列出 所有 Python 教程。