ZetCode

测试响应式布局

最后修改于 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 应用提供了一个具有圣杯布局的单个页面。

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

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

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),确保无论内容多少都能覆盖整个页面。

为了增强适应性,媒体查询会为不同的屏幕尺寸调整网格布局。在 1024px 以下的宽度下,布局会变为两列,主内容堆叠在侧边栏之上。在 768px 以下,网格会折叠为单列布局,所有部分垂直堆叠。这种方法确保了跨设备的易用性界面,在小屏幕上保持可读性和易于导航。这是响应式 Web 开发实用且高效的设计。

Selenium 测试

测试跨视口验证布局和可见性。将其另存为 test/test_layout.py

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 用于自动化浏览器测试。setUpClasstearDownClass 方法负责服务器和 WebDriver 的生命周期管理,而 setUp 方法通过在每次测试前清除 cookie 来确保干净的浏览器状态。

主要关注点是 test_layout_responsiveness_and_visibility 方法,该方法跨多个视口验证布局:桌面、平板电脑和移动设备。使用 Selenium,它动态调整浏览器尺寸并检查 CSS 属性,如 grid-areawidthdisplay,以确保所有元素都正确放置并可见。自定义辅助方法,如 get_computed_styleget_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 测试服务器,作为跨多种设备进行布局验证的基础。通过详细的测试,使用对应于桌面、平板电脑和移动设备环境的视口,验证了布局的适应性。

作者

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

列出 所有 Python 教程