在线点播系统-测试

0 测试结果

接口测试:采用 Pytest + Requests 进行测试,验证了底层数据流转与高并发场景下的播放量累加逻辑。设计 46 个正逆向测试用例,核心逻辑覆盖率达 85% 以上,测试期间发现并修复 7 个 Bug

UI 测试:使用 Playwright 框架进行测试,实现了 4 大核心业务链路(涵盖视频增删改查闭环、搜索与评论动态回显等)的场景覆盖。用例内部拆解了 30+ 个强交互断言节点


1 测试脚本

1.1 基于 Requests 的接口自动化测试

在现代 Web 开发中,前后端分离架构使得 API 成为了系统的命脉。本小节主要使用 Pytest + Requests 库,对在线点播系统后端接口进行测试。

1.1.0 接口自动化文档 (涵盖 46 个核心测试用例)
  • test_video_list_and_search (C端搜索与列表接口)

    • 测试目的:验证主页视频列表接口 (/api/connect_PgSql) 的可用性、搜索过滤准确性以及恶意输入防御能力。
    • 核心逻辑:参数化驱动 5 个用例(全量查询、正常关键字、特殊字符、超长边界字符串、SQL 注入探测),断言 HTTP 状态码为 200,并校验返回格式为 List。
    • 设计:针对 SQL 注入(如 ' OR '1'='1)和特殊字符进行深度安全断言,强制验证当遭遇恶意攻击负载时,后端能有效拦截并返回空列表,避免数据库报错或数据泄露。
  • test_get_video_details (视频详情与资源状态路由校验)

    • 测试目的:验证单个视频详情接口 (/api/get_video_details) 的数据获取完整性,以及防盗链机制与非数字 ID 拦截防线。
    • 核心逻辑:参数化驱动 5 个用例(有效 ID、超大不存在 ID、负数 ID、字符串注入、空参)。断言有效 ID 返回 200,不存在 ID 返回 404,非法格式前置拦截返回 400。
    • 设计:通过 RUNTIME_DATA 动态提取真实有效 ID 避免硬编码;并深度断言 JSON 结构中包含 signed_play_url,确保 MD5 时间戳防盗链签名被正确计算与下发。
  • test_add_comment_boundaries & test_add_comment_method_not_allowed (新增评论边界与方法拦截)

    • 测试目的:严格验证评论发布接口 (/api/add_comment) 的输入边界、跨站脚本 (XSS) 容忍度以及 HTTP 方法合规性。
    • 核心逻辑:参数化驱动 9 个用例(包含正常发布、空内容、纯空格、XSS 盲打 <script>、2000字超长压力测试、缺少外键 ID,以及 GET/PUT/DELETE 请求方法阻断)。
    • 设计:引入了 time.sleep(0.2) 缓冲机制,有效解决瞬时极高并发(瞬间发起数十个带有大 payload 请求)导致的 Serverless 数据库连接池被打满问题;同时精准验证了 405 Method Not Allowed 的 RESTful 语义规范。
  • test_update_view_methods & test_update_view_exceptions (播放量更新并发与异常探测)

    • 测试目的:验证底层播放量 +1 运算接口 (/api/update_view) 对多种 HTTP 动词的支持及对非法参数的隔离。
    • 核心逻辑:参数化驱动 6 个用例。验证 GET 和 POST 双方法均能返回 204 No Content;针对不存在 ID 严格断言 404(通过 SQL 的 RETURNING 长度判断),针对负数和非法字符串断言 400。
  • test_manage_videos_pagination_and_search (后台列表分页与边界容错测试)

    • 测试目的:验证管理后台视频列表 (/api/manage-videos) 的分页、联合搜索逻辑,以及防止内存溢出 (OOM) 的兜底防线。
    • 核心逻辑:参数化驱动 6 个用例(标准请求、分页叠加搜索、page=0、page=-1、超大 limit 值、注入非法字符串)。
    • 设计:极致的后端容错探测。当故意传入 limit=99999 时,不仅断言返回 200,更通过 len(videos) <= 10 的深度断言,验证后端是否成功触发了强制修正逻辑,完美防御了黑客恶意拉跨数据库的攻击。
  • test_manage_comments (多维检索与 CRUD 数据清理闭环)

    • 测试目的:验证评论后台 (/api/manage-comments) 的双向联合检索能力,以及自动化测试框架的“造数据-查数据-删数据”闭环扫尾能力。
    • 核心逻辑:参数化检索和异常删除共 9 个用例(评论/视频单维搜索、联合搜索、注入防御,以及缺失/越界/非法 ID 删除)。外加 1 个完整的生命周期闭环测试。
    • 设计:实现了测试数据零残留。通过 POST 接口先造出一条带有“测试靶点”特征的脏数据,调用 GET 接口利用模糊搜索动态锁定它的 comment_id,再调用 DELETE 接口物理抹除。最后再发一次 DELETE 请求断言返回 404,严格验证了接口的幂等性和数据销毁的彻底性。
  • test_analytics_methods_and_data (后台统计大屏聚合接口)

    • 测试目的:验证统计面板聚合 API (/api/analytics) 的并发查询有效性及数据结构合规性。
    • 核心逻辑:参数化驱动 4 个用例(GET 数据获取,以及 POST/PUT/DELETE 的 405 拦截)。
    • 设计:针对 200 成功的请求进行深度结构校验,严格断言 topVideostopTagsByViewstopTagsByCount 三个板块不仅存在,且数据类型必须为合法的 List 列表。
  • test_get_view_counts_structure (全局播放量缓存击穿与结构断言)

    • 测试目的:验证全站播放量字典接口 (/api/get_view_counts) 的返回格式与缓存穿透能力。
    • 核心逻辑:执行 1 个核心用例。在 URL 末尾动态拼接毫秒级时间戳(?_t=xxx)强行绕过 Vercel 边缘缓存,确保拿到数据库绝对实时数据。

1.1.1 接口自动化测试脚本
import pytest
import requests
import time

# 全局配置
BASE_URL = "https://xxx.cn" # 如果是本地环境,请替换为 http://localhost:3000;否则 https://xxx.cn
TIMEOUT = 20

# 模块级全局变量,用于存储动态获取的依赖数据
RUNTIME_DATA = {
    "valid_video_id": None,
    "valid_comment_id": None
}

@pytest.fixture(scope="module", autouse=True)
def setup_test_data():
    """
    前置夹具 (Fixture):在所有测试开始前,先获取一个真实的 video_id,
    供后续所有需要 videoId 的异常测试使用。
    """
    resp = requests.get(f"{BASE_URL}/api/connect_PgSql", timeout=TIMEOUT)
    if resp.status_code == 200 and len(resp.json()) > 0:
        RUNTIME_DATA["valid_video_id"] = resp.json()[0]["id"]
    else:
        pytest.skip("严重警告:数据库中没有视频,无法进行后续依赖视频的测试。")


class TestComprehensiveAPIs:

    # =======================================================
    # 模块 1:C端搜索与列表接口 (累计 5 个用例)
    # 路由: /api/connect_PgSql
    # =======================================================
    @pytest.mark.parametrize("search_term, expected_status", [
        ("", 200),                          # 用例 1: 正常全量查询
        ("风景", 200),                      # 用例 2: 正常关键字查询
        ("!@#$%^&*()", 200),                # 用例 3: 特殊字符查询 (测试 SQL 注入防御和崩溃)
        ("A" * 500, 200),                   # 用例 4: 超长字符串边界测试
        ("' OR '1'='1", 200),               # 用例 5: 经典 SQL 注入探测
    ])
    def test_video_list_and_search(self, search_term, expected_status):
        url = f"{BASE_URL}/api/connect_PgSql"
        params = {"search": search_term} if search_term else {}
        resp = requests.get(url, params=params, timeout=TIMEOUT)
        
        assert resp.status_code == expected_status
        data = resp.json()
        assert isinstance(data, list)
        
        # 进阶断言:如果是乱码或注入,结果必须是空列表
        if search_term in ["!@#$%^&*()", "A" * 500, "' OR '1'='1"]:
            assert len(data) == 0, f"警告:搜索 {search_term} 竟然查出了数据!可能存在漏洞!"

    # =======================================================
    # 模块 2:视频详情与防盗链接口 (累计 5 个用例)
    # 路由: /api/get_video_details
    # =======================================================
    @pytest.mark.parametrize("video_id, expected_status, expect_error", [
        ("VALID_ID", 200, False),           # 用例 6: 正常 ID
        ("99999999", 404, True),            # 用例 7: 不存在的极大数据
        ("-1", 400, True),                  # 用例 8: 负数 ID
        ("abc", 400, True),                 # 用例 9: 字符串非法类型注入
        ("", 400, True),                    # 用例 10: 空参拦截测试
    ])
    def test_get_video_details(self, video_id, expected_status, expect_error):
        # 动态替换有效的 ID
        vid = RUNTIME_DATA["valid_video_id"] if video_id == "VALID_ID" else video_id
        url = f"{BASE_URL}/api/get_video_details?id={vid}"
        resp = requests.get(url, timeout=TIMEOUT)
        
        assert resp.status_code == expected_status
        if expect_error:
            assert "error" in resp.json()
        else:
            data = resp.json()
            assert data["id"] == vid
            assert "signed_play_url" in data  # 强校验防盗链 URL 是否生成


    # =======================================================
    # 模块 3:新增评论接口 (累计 6 个用例)
    # 路由: /api/add_comment
    # =======================================================
    @pytest.mark.parametrize("use_valid_vid, content, expected_status", [
        (True, "正常测试评论", 201),                      # 用例 11: 正常发布
        (True, "", 400),                                # 用例 12: 内容为空阻断
        (True, "   ", 400),                             # 用例 13: 纯空格阻断
        (True, "<script>alert('xss')</script>", 201),   # 用例 14: XSS 盲打测试 (后端应允许存入,前端转义)
        (True, "A" * 2000, 201),                        # 用例 15: 超长文本压力测试
        (False, "缺少视频ID", 400),                     # 用例 16: 必填外键丢失
    ])
    def test_add_comment_boundaries(self, use_valid_vid, content, expected_status):
        url = f"{BASE_URL}/api/add_comment"
        payload = {"content": content}
        if use_valid_vid:
            payload["videoId"] = RUNTIME_DATA["valid_video_id"]
            
        # 💡 解决方案 1:每次请求前微微停顿 0.2 秒,给 Neon 数据库喘息和释放连接的时间
        time.sleep(0.2)
            
        resp = requests.post(url, json=payload, timeout=TIMEOUT)
        assert resp.status_code == expected_status

    # =======================================================
    # 模块 4:播放量递增并发与缓存击穿 (累计 6 个用例)
    # 路由: /api/update_view
    # =======================================================
    @pytest.mark.parametrize("method", ["GET", "POST"])     # 用例 17, 18: 支持双请求方法
    def test_update_view_methods(self, method):
        vid = RUNTIME_DATA["valid_video_id"]
        url = f"{BASE_URL}/api/update_view?id={vid}"
        resp = requests.request(method, url, timeout=TIMEOUT)
        assert resp.status_code == 204

    # 精准断言资源不存在 (404) 与类型非法 (400)
    @pytest.mark.parametrize("invalid_id, expected_status", [
        ("99999999", 404),  # 用例 19: 更新不存在的ID,必须返回 404
        ("abc", 400),       # 用例 20: 非法字符 ID,必须前置拦截返回 400
        ("-5", 400),        # 用例 21: 负数 ID 拦截
        ("", 400),          # 用例 22: 丢失 ID 阻断
    ])

    def test_update_view_exceptions(self, invalid_id, expected_status):
        url = f"{BASE_URL}/api/update_view?id={invalid_id}"
        resp = requests.get(url, timeout=TIMEOUT)
        assert resp.status_code == expected_status

    # =======================================================
    # 模块 5:后台管理 - 列表分页边界测试 (累计 6 个用例)
    # 路由: /api/manage-videos (GET)
    # =======================================================
    # 分页与搜索组合测试
    @pytest.mark.parametrize("page, limit, search, expected_status", [
        (1, 5, "", 200),            # 用例 23: 标准请求
        (2, 5, "风景", 200),        # 用例 24: (新增) 分页叠加搜索条件
        (0, 5, "", 200),            # 用例 25: page 为 0 (后端应自动纠正为 1)
        (-1, 5, "", 200),           # 用例 26: page 为负数 (后端应自动纠正为 1)
        (1, 99999, "", 200),        # 用例 27: 极大 limit (后端应自动限制为 10)
        ("abc", "xyz", "", 200),    # 用例 28: 类型注入 (后端应 fallback 到默认值 1 和 5)
    ])
    def test_manage_videos_pagination_and_search(self, page, limit, search, expected_status):
        url = f"{BASE_URL}/api/manage-videos?page={page}&limit={limit}&search={search}"
        resp = requests.get(url, timeout=TIMEOUT)
        assert resp.status_code == expected_status
        
        data = resp.json()
        assert "videos" in data and "pagination" in data
    

    # =======================================================
    # 模块 6:评论管理与数据闭环扫尾 (累计 10 个用例)
    # 路由: /api/manage-comments (GET, DELETE)
    # =======================================================
    @pytest.mark.parametrize("comment_search, video_search", [
        ("", ""),                     # 用例 29: 全量
        ("测试", ""),                 # 用例 30: 仅搜评论
        ("", "风景"),                 # 用例 31: 仅搜视频
        ("测试", "风景"),             # 用例 32: 联合检索
        ("'; DROP TABLE comments;--", ""), # 用例 33: 联合检索 SQL 注入防御
    ])
    def test_manage_comments_search(self, comment_search, video_search):
        url = f"{BASE_URL}/api/manage-comments?commentSearch={comment_search}&videoSearch={video_search}"
        resp = requests.get(url, timeout=TIMEOUT)
        assert resp.status_code == 200
        assert "comments" in resp.json()

    @pytest.mark.parametrize("comment_id, expected_status", [
        ("", 400),                  # 用例 34: 缺省ID
        ("99999999", 404),          # 用例 35: 不存在的ID
        ("abc", 400),               # 用例 36: 类型错误
        ("-1", 400),               # 用例 37: 类型错误
    ])
    def test_manage_comments_delete_exceptions(self, comment_id, expected_status):
        url = f"{BASE_URL}/api/manage-comments?commentId={comment_id}"
        resp = requests.delete(url, timeout=TIMEOUT)
        assert resp.status_code == expected_status


    def test_manage_comments_delete_success(self):
        """
        用例 38: 完整的评论删除闭环
        先造一条脏数据,获取它的 ID,然后执行删除,最后断言找不到了。
        """
        # 1. 造数据
        vid = RUNTIME_DATA["valid_video_id"]
        post_resp = requests.post(f"{BASE_URL}/api/add_comment", json={"videoId": vid, "content": "【删除闭环测试】靶点"})
        assert post_resp.status_code == 201

        # 2. 查出它的 ID
        get_resp = requests.get(f"{BASE_URL}/api/manage-comments?commentSearch=【删除闭环测试】靶点")
        comments = get_resp.json().get("comments", [])
        assert len(comments) > 0
        target_id = comments[0]["comment_id"]

        # 3. 执行删除
        del_resp = requests.delete(f"{BASE_URL}/api/manage-comments?commentId={target_id}")
        assert del_resp.status_code == 200

        # 4. 再次删除同一条 (测试幂等性/404)
        del_again_resp = requests.delete(f"{BASE_URL}/api/manage-comments?commentId={target_id}")
        assert del_again_resp.status_code == 404
    # =======================================================
    # 模块 7:增加 HTTP 请求方法拦截测试 (累计 3 个用例)
    # 路由: /api/add_comment
    # =======================================================
    @pytest.mark.parametrize("method, expected_status", [
        ("GET", 405),# 用例 39: 类型错误
        ("PUT", 405),# 用例 40: 类型错误
        ("DELETE", 405),  # 用例 41: 类型错误
    ])
    def test_add_comment_method_not_allowed(self, method, expected_status):
        url = f"{BASE_URL}/api/add_comment"
        resp = requests.request(method, url, timeout=TIMEOUT)
        assert resp.status_code == expected_status



    # =======================================================
    # 模块 8:后台统计面板接口 (累计 4 个用例)
    # 路由: /api/analytics
    # =======================================================
    @pytest.mark.parametrize("method, expected_status", [
        ("GET", 200),     # 用例 42: 正常获取数据
        ("POST", 405),    # 用例 43: 方法不被允许
        ("PUT", 405),     # 用例 44: 方法不被允许
        ("DELETE", 405),  # 用例 45: 方法不被允许
    ])
    def test_analytics_methods_and_data(self, method, expected_status):
        url = f"{BASE_URL}/api/analytics"
        resp = requests.request(method, url, timeout=TIMEOUT)
        assert resp.status_code == expected_status
        
        # 对于 200 成功的请求,深度断言其返回的 JSON 结构
        if expected_status == 200:
            data = resp.json()
            assert "topVideos" in data, "缺少 topVideos 字段"
            assert "topTagsByViews" in data, "缺少 topTagsByViews 字段"
            assert "topTagsByCount" in data, "缺少 topTagsByCount 字段"
            # 进一步断言:这三个字段的值必须是列表
            assert isinstance(data["topVideos"], list)
            assert isinstance(data["topTagsByViews"], list)
            assert isinstance(data["topTagsByCount"], list)


    # =======================================================
    # 模块 9:全站播放量字典接口 (累计 1 个用例)
    # 路由: /api/get_view_counts
    # =======================================================
    def test_get_view_counts_structure(self):
        """
        用例 46: 正常获取全站播放量字典,并断言其为合法的 Dictionary 对象
        """
        url = f"{BASE_URL}/api/get_view_counts"
        
        # 加上时间戳击穿缓存,确保拿到的是数据库实时结构
        timestamp = int(time.time() * 1000)
        resp = requests.get(f"{url}?_t={timestamp}", timeout=TIMEOUT)
        
        assert resp.status_code == 200
        data = resp.json()
        
        # 核心断言:必须是一个字典 { "1": 100, "2": 50 }
        assert isinstance(data, dict), "全站播放量返回的数据结构必须是字典(dict)"
        
        # 边界抽样测试:如果字典不为空,随便取一个值看看是不是整数(或者能转成整数的字符串)
        if len(data) > 0:
            random_value = list(data.values())[0]
            try:
                int(random_value)
            except ValueError:
                pytest.fail(f"播放量的值不合法,应该是数字,实际是: {random_value}")  

1.1.2 pytest-html 测试报告
pytest -s -v --html=report.html --self-contained-html 40_tets.py
# 运行当前文件并生成report.html测试报告
PS F:\my-video-app> pytest -s -v --html=report.html --self-contained-html 40_tets.py
======================================================== test session starts ========================================================
platform win32 -- Python 3.7.8, pytest-7.4.4, pluggy-1.2.0 -- E:\python\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Windows-10-10.0.22621-SP0', 'Packages': {'pytest': '7.4.4', 'pluggy': '1.2.0'}, 'Plugins': {'allure-pytest': '2.15.3', 'anyio': '3.7.1', 'base-url': '2.0.0', 'cov': '4.1.0', 'html': '3.2.0', 'metadata': '3.0.0', 'playwright': '0.3.3'}, 'JAVA_HOME': 'F:\\jdk_1.8.0_151'}
rootdir: F:\my-video-app
plugins: allure-pytest-2.15.3, anyio-3.7.1, base-url-2.0.0, cov-4.1.0, html-3.2.0, metadata-3.0.0, playwright-0.3.3
collected 46 items

40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[!@#$%^&*()-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[' OR '1'='1-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[VALID_ID-200-False] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[99999999-404-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-1-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[abc-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-\u6b63\u5e38\u6d4b\u8bd5\u8bc4\u8bba-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True--400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-   -400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-<script>alert('xss')</script>-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[False-\u7f3a\u5c11\u89c6\u9891ID-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[GET] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[POST] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-5-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[2-5-\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[0-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[-1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-99999--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[abc-xyz--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search['; DROP TABLE comments;---] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-1-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_success PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[GET-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[GET-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[POST-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_view_counts_structure PASSED

------------------------------------- generated html file: file:///F:/my-video-app/report.html -------------------------------------- 
======================================================== 46 passed in 58.23s ======================================================== 

pytest-html 测试报告截图

pytest-html 测试报告
在这里插入图片描述


1.1.3 Allure 生成测试报告
pytest -s -v --alluredir=./allure-results 40_tets.py 
#执行测试并生成临时数据
allure serve ./allure-results # 启动 Allure 报告面板
PS F:\my-video-app> pytest -s -v --alluredir=./allure-results 40_tets.py 
======================================================== test session starts ========================================================
platform win32 -- Python 3.7.8, pytest-7.4.4, pluggy-1.2.0 -- E:\python\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Windows-10-10.0.22621-SP0', 'Packages': {'pytest': '7.4.4', 'pluggy': '1.2.0'}, 'Plugins': {'allure-pytest': '2.15.3', 'anyio': '3.7.1', 'base-url': '2.0.0', 'cov': '4.1.0', 'html': '3.2.0', 'metadata': '3.0.0', 'playwright': '0.3.3'}, 'JAVA_HOME': 'F:\\jdk_1.8.0_151'}
rootdir: F:\my-video-app
plugins: allure-pytest-2.15.3, anyio-3.7.1, base-url-2.0.0, cov-4.1.0, html-3.2.0, metadata-3.0.0, playwright-0.3.3
collected 46 items

40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[!@#$%^&*()-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[' OR '1'='1-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[VALID_ID-200-False] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[99999999-404-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-1-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[abc-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-\u6b63\u5e38\u6d4b\u8bd5\u8bc4\u8bba-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True--400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-   -400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-<script>alert('xss')</script>-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[False-\u7f3a\u5c11\u89c6\u9891ID-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[GET] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[POST] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-5-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[2-5-\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[0-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[-1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-99999--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[abc-xyz--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search['; DROP TABLE comments;---] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-1-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_success PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[GET-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[GET-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[POST-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_view_counts_structure PASSED

======================================================== 46 passed in 52.26s ========================================================

启动 Allure 报告面板
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
935b2089841bc876d8d7af9199eff.png)
在这里插入图片描述


1.2 基于 Playwright 的UI自动化测试

为了模拟真实用户的操作路径,本部分采用当下最先进的现代自动化框架 Playwright,实现前端交互与后端接口拦截的完美融合。

1.2.0 Playwright UI自动化文档
  • test_c_end_user_core_journey (用户核心体验链路)

    • 业务场景:模拟普通用户从打开首页、搜索视频、点击播放到发表评论的完整旅程。

    • 设计 (网络拦截与显式等待):抛弃了不稳定的 time.sleep(),采用 Playwright 的 page.expect_response() 机制。例如,点击搜索按钮后,脚本会挂起,直到精准拦截到 /api/connect_PgSql?search=... 接口返回 200 OK,且前端 DOM 树成功渲染出对应的视频卡片后,才进行下一步断言。同时,测试了不刷新页面的情况下,新发表的评论是否通过 DOM 操作成功置顶回显。

  • test_b_end_comment_audit_and_cleanup (管理员评论审核与管控链路)

    • 业务场景:模拟管理员登录后台,检索特定关键字违规评论并批量清理。

    • **设计 **:在删除多条评论时,每次点击删除都会导致 DOM 刷新。此用例通过 while 循环,动态检测页面上剩余的“删除”按钮并永远只点击第一个,规避了传统自动化测试中极易触发的“元素陈旧异常”。

  • test_b_end_video_crud_lifecycle (管理员视频生命周期闭环: 物理直传与销毁)

    • 业务场景:完整模拟大文件(视频+封面图)的真实物理上传、跨面板状态同步、二次编辑以及物理文件的销毁。

    • 设计 (动态 ID 流转与长耗时接管)

      1. 超长超时机制:物理上传直传云端非常耗时,代码中专门针对 POST /api/manage-videos 接口赋予了 60 秒的超长等待。

      2. 动态靶向定位:成功上传后,脚本会从后端的 JSON 响应中“拿到” newVideoId。在随后的修改和删除操作中,不需要遍历表格,而是直接利用 CSS 选择器 tr[data-video-id="{newVideoId}"] 实施精准靶向打击。

      3. UI 遮罩层攻克:精准识别并处理前端 Alert 模态框,防止遮罩层阻断后续的自动化点击。

  • test_b_end_video_complex_update (管理员复杂更新交互: 标签增删与物理替换)

    • 业务场景:测试高难度的前端交互组件(如自定义 Tag 输入框)及底层存储文件更替逻辑。

    • 核心逻辑:极速构建一个临时测试视频后,模拟精准点击 Tag 的 × 按钮删除特定标签,并在自定义输入框中敲击 Enter 生成新标签。随后,利用 set_input_files 将底层的封面和视频文件进行强制替换。最后通过接口断言,确认腾讯云 COS 的新文件上传与旧文件删除机制正常触发。


1.2.1 接口自动化测试脚本
import pytest
import time
import re
from playwright.sync_api import Page, expect
import urllib.parse
import os
# ==========================================
# ⚙️ 浏览器全局配置:解决没有全屏的问题
# ==========================================
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
    """配置浏览器启动参数,使其默认最大化"""
    return {
        **browser_type_launch_args,
        "args": ["--start-maximized"]
    }

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    """禁用 Playwright 默认的固定视口,跟随系统窗口大小"""
    return {
        **browser_context_args,
        "no_viewport": True
    }

BASE_URL = "https://xxx.cn" 

# 第一个函数:用户访问+评论
def test_c_end_user_core_journey(page: Page):
    """
    测试套件一:C 端用户核心体验链路 (E2E)
    """
    
    # ---------------------------------------------------------
    # 🎬 步骤 1:访问首页,等待视频列表加载与 DOM 渲染
    # ---------------------------------------------------------
    print("\n[Step 1] 访问首页,等待视频列表加载...")
    with page.expect_response("**/api/connect_PgSql*") as response_info:
        page.goto(f"{BASE_URL}/index.html")
    assert response_info.value.ok
    
    # 严格等待前端 DOM 渲染完成:确保“视频加载中...”消失,且至少一个视频卡片渲染完毕
    expect(page.locator("#video-grid p:has-text('视频加载中...')")).not_to_be_visible()
    page.wait_for_selector(".video-card", state="visible")
    
    # 【视觉停顿】观赏 (3秒)
    page.wait_for_timeout(3000) 


    # ---------------------------------------------------------
    # 🎬 步骤 2:模拟搜索操作,验证列表过滤
    # ---------------------------------------------------------
    print("[Step 2] 模拟用户输入搜索词并搜索...")
    first_video_title = page.locator(".video-card").first.locator("h3").inner_text()
    search_term = first_video_title[:2] if len(first_video_title) > 2 else first_video_title

    page.locator("#search-input").fill(search_term)
    
    with page.expect_response(re.compile(r".*/api/connect_PgSql\?search=.*")) as search_resp:
        page.locator("#search-form button").click()
    assert search_resp.value.ok
    
    # 严格等待前端 DOM 刷新:标题变更,且新的结果卡片渲染出来
    expect(page.locator("#main-title")).to_have_text(f'搜索: "{search_term}"')
    page.wait_for_selector(".video-card", state="visible")
    
    target_video_card = page.locator(".video-card").first
    target_video_title = target_video_card.locator("h3").inner_text()
    
    page.wait_for_timeout(1500) 


    # ---------------------------------------------------------
    # 🎬 步骤 3:点击视频卡片,验证底层 update_view 触发及跳转
    # ---------------------------------------------------------
    print("[Step 3] 点击视频,验证跳转及详情加载...")
    with page.expect_response("**/api/get_video_details*") as details_resp:
        target_video_card.click()
    assert details_resp.value.ok
    expect(page).to_have_url(re.compile(r".*player\.html\?id=\d+"))


    # ---------------------------------------------------------
    # 🎬 步骤 4:播放页渲染断言
    # ---------------------------------------------------------
    print("[Step 4] 严格验证播放页 DOM 渲染...")
    # 必须等待标题不再是占位符“加载中...”
    expect(page.locator("#video-title")).not_to_have_text("加载中...")
    expect(page.locator("#video-title")).to_have_text(target_video_title)
    expect(page.locator("#video-wrapper video")).to_be_visible()
    
    page.wait_for_timeout(1500)


    # ---------------------------------------------------------
    # 🎬 步骤 5:交互评论与无刷新 DOM 回显验证
    # ---------------------------------------------------------
    print("[Step 5] 模拟用户输入带时间戳的评论并发布...")
    timestamp = int(time.time())
    test_comment = f"【Playwright自动化测试】零延迟极速体验!时间戳:{timestamp}"
    
    page.locator("#comment-input").fill(test_comment)
    
    with page.expect_response("**/api/add_comment") as comment_resp:
        page.locator("#comment-submit-btn").click()
    assert comment_resp.value.status == 201
    
    # 等待前端 JS 把新评论 append 到列表顶部
    first_comment_locator = page.locator(".comment-content").first
    expect(first_comment_locator).to_have_text(test_comment)
    
    # 最后停留3秒,让你看清楚发布的评论再关闭浏览器
    page.wait_for_timeout(3000)
    print("✅ 用户核心体验链路测试通过,丝滑且清晰!")




# 第二个函数:搜索对应评论 + 循环删除 + 彻底清理
def test_b_end_comment_audit_and_cleanup(page: Page):
    """
    测试套件三:B 端评论审核与管控链路
    覆盖场景:进入管理后台 -> 切换评论 Tab -> 搜索包含“时间戳”的测试评论 -> 循环精准删除 -> 验证数据彻底清空
    """
    
    # ---------------------------------------------------------
    # 🎬 步骤 1:访问管理后台并切换到评论 Tab
    # ---------------------------------------------------------
    print("\n[Step 1] 访问管理后台,切换至评论管理...")
    page.goto(f"{BASE_URL}/management.html")
    
    # 点击侧边栏的“评论管理” Tab,并监听随之触发的数据请求
    with page.expect_response("**/api/manage-comments*") as init_resp:
        page.locator('.nav-link[data-tab="comments"]').click()
    assert init_resp.value.ok
    
    # 等待评论面板激活,且状态提示消失
    expect(page.locator("#comments-panel")).to_have_class(re.compile(r"active"))
    page.wait_for_timeout(1000) # 视觉停顿


    # ---------------------------------------------------------
    # 🎬 步骤 2:多维搜索特定的测试评论
    # ---------------------------------------------------------
    print("[Step 2] 搜索带有“时间戳”关键字的测试评论...")
    search_keyword = "时间戳"


    page.locator("#comments-search-content").fill(search_keyword)
    # 将 "时间戳" 转换为 "%E6%97%B6%E9%97%B4%E6%88%B3"
    encoded_keyword = urllib.parse.quote(search_keyword)
    
    with page.expect_response(re.compile(rf".*/api/manage-comments.*commentSearch={encoded_keyword}.*")) as search_resp:
        page.locator("#comments-search-btn").click()
    assert search_resp.value.ok
    
    page.wait_for_timeout(1000) # 视觉停顿


    # ---------------------------------------------------------
    # 🎬 步骤 3:循环清理所有匹配的测试数据 (规避 Stale Element)
    # ---------------------------------------------------------
    print("[Step 3] 开始循环清理测试脏数据...")
    delete_buttons = page.locator("#comments-table-body .btn-action-delete")
    
    # 只要列表里还有删除按钮,就一直删
    while delete_buttons.count() > 0:
        count_before = delete_buttons.count()
        print(f"  -> 当前列表还有 {count_before} 条测试评论,准备删除第一条...")
        
        # 1. 点击第一个删除按钮
        delete_buttons.first.click()
        
        # 2. 等待确认弹窗完全显示
        expect(page.locator("#delete-comment-modal")).to_have_class(re.compile(r"active"))
        
        # 3. 点击确认删除,并严格监听 DELETE 和自动触发的 GET 刷新接口
        with page.expect_response("**/api/manage-comments*commentId=*") as del_resp:
            page.locator("#delete-comment-confirm-btn").click()
        
        # 断言删除接口成功
        assert del_resp.value.request.method == "DELETE"
        assert del_resp.value.ok
        
        # 4. 隐式等待:Playwright 会在下一次 while 循环检查 count() 时自动等待 DOM 稳定
        # 为了肉眼观赏,稍微停顿一下
        page.wait_for_timeout(800) 

    print("✅ 所有“时间戳”相关的测试评论已清理完毕!")


    # ---------------------------------------------------------
    # 🎬 步骤 4:闭环断言
    # ---------------------------------------------------------
    print("[Step 4] 终极断言:验证表格内已无相关数据...")
    expect(page.locator("#comments-table-body")).to_contain_text("未找到评论。")
    page.wait_for_timeout(1500)
    print("🎉 B端评论审核与管控链路测试通过!数据库干干净净!")



# ==========================================
# ⚙️ 真实的物理测试
# ==========================================
COVER_PATH = r"D:\temp_video\covers\17_beautifulword.jpg"
VIDEO_PATH = r"D:\temp_video\17_beautifulword.mp4"

# 第三个函数:视频 CRUD 全生命周期管理:从物理上传到跨面板修改再到彻底删除,完整覆盖 B 端视频管理的核心链路
def test_b_end_video_crud_lifecycle(page: Page):
    """
    测试套件二:B 端视频全生命周期管理 (CRUD 闭环)
    覆盖场景:物理大文件上传 -> 跨面板动态 ID 追踪 -> 信息精准修改 -> 数据与文件的彻底物理销毁
    """
    
    # 前置检查:确保本地测试物料真的存在,防止因为路径写错直接导致用例失败
    assert os.path.exists(COVER_PATH), f"❌ 找不到测试封面图: {COVER_PATH}"
    assert os.path.exists(VIDEO_PATH), f"❌ 找不到测试视频: {VIDEO_PATH}"

    # ---------------------------------------------------------
    # 🎬 阶段一 & 二:模拟真实物理上传 (Create)
    # ---------------------------------------------------------
    print("\n[Step 1] 访问管理后台,准备执行物理文件上传...")
    page.goto(f"{BASE_URL}/management.html")
    
    # 填写基础文本信息
    page.locator("#upload-title").fill("你是不是很久没见过这个美丽的世界了")
    page.locator("#upload-description").fill("光与影的褶皱里,藏着世界未说出口的温柔。")
    page.wait_for_timeout(500) 
    
    # 攻克自定义标签组件:填入文本后敲击 Enter 键生成 Tag
    page.locator("#upload-tag-container .tag-input-field").fill("风景")
    page.locator("#upload-tag-container .tag-input-field").press("Enter")
    page.wait_for_timeout(1500) 
    # 断言标签组件是否成功生成了带有 "风景" 文本的 chip 元素
    expect(page.locator("#upload-tag-container .tag-chip")).to_contain_text("风景")
    
    # 挂载真实的物理文件到底层 <input type="file"> 中
    page.locator("#upload-cover-input").set_input_files(COVER_PATH)
    page.locator("#upload-video-input").set_input_files(VIDEO_PATH)
    page.wait_for_timeout(2000) 
    
    print("[Step 2] 提交上传,正在等待 COS 与数据库响应 (耗时较长,请耐心等待)...")
    # 核心难点:真实的上传非常耗时,必须给这个接口极其宽裕的超时时间 (这里设为 60000 毫秒 / 60 秒)
    with page.expect_response("**/api/manage-videos", timeout=60000) as upload_resp:
        page.locator("#upload-submit-btn").click()
    
    # 提取极其宝贵的动态数据:从后端响应中精准捕获新生成的 videoId
    assert upload_resp.value.ok, "视频上传接口报错!"
    upload_data = upload_resp.value.json()
    new_video_id = upload_data.get("video", {}).get("newVideoId")
    assert new_video_id is not None, "未能从上传响应中提取到 newVideoId!"
    print(f"✅ 上传成功!成功捕获新视频 ID: {new_video_id}")
    page.wait_for_timeout(1000) 
    
    # 处理 UI 阻塞:点击上传成功后的全局 Alert 确认弹窗
    expect(page.locator("#alert-modal")).to_have_class(re.compile(r"active"))
    page.locator("#alert-modal-ok-btn").click()
    page.wait_for_timeout(1000) 

    # ---------------------------------------------------------
    # 🎬 阶段三:跨面板状态同步与精准修改 (Read & Update)
    # ---------------------------------------------------------
    print("[Step 3] 切换至视频管理面板,执行数据修改...")
    # 切换 Tab 并等待列表接口返回
    with page.expect_response("**/api/manage-videos*page=1*") as list_resp:
        page.locator('.nav-link[data-tab="management"]').click()
    assert list_resp.value.ok
    
    # 使用上一步拿到的动态 ID,在茫茫表格中精准锁定我们要操作的那一行
    target_row = page.locator(f'tr[data-video-id="{new_video_id}"]')
    
    # 断言刚刚上传的数据已经正确渲染在表格中了
    expect(target_row).to_be_visible()
    page.wait_for_timeout(1000) 
    # 点击那一行的“编辑”按钮
    target_row.locator(".btn-action-edit").click()
    expect(page.locator("#edit-modal")).to_have_class(re.compile(r"active"))
    page.wait_for_timeout(1000) 
    # 在原标题基础上加上修改标识
    new_title = "你是不是很久没见过这个美丽的世界了 - [UI自动化修改]"
    page.locator("#edit-title").fill(new_title)
    page.wait_for_timeout(1000) 
    # 再追加一个测试标签
    page.locator("#edit-tag-container .tag-input-field").fill("自动化测试")
    page.locator("#edit-tag-container .tag-input-field").press("Enter")
    page.wait_for_timeout(1000) 
    print("  -> 保存修改信息...")
    with page.expect_response("**/api/manage-videos", timeout=30000) as edit_resp:
        page.locator("#edit-save-btn").click()
        
    assert edit_resp.value.request.method == "PUT", "调用的不是 PUT 更新接口!"
    assert edit_resp.value.ok
    
    # 模态框应该自动关闭,且表格里的标题应该发生改变
    expect(page.locator("#edit-modal")).not_to_have_class(re.compile(r"active"))
    expect(target_row).to_contain_text("[UI自动化修改]")
    print("✅ 视频信息修改成功,UI 状态已同步更新!")

    page.wait_for_timeout(2000) 
    # ---------------------------------------------------------
    # 🎬 阶段四:物理与逻辑的彻底销毁 (Delete & Teardown)
    # ---------------------------------------------------------
    print("[Step 4] 执行测试扫尾:彻底删除该视频与关联文件...")
    # 依然精准打击这一行,点击删除
    target_row.locator(".btn-action-delete").click()
    expect(page.locator("#delete-confirm-modal")).to_have_class(re.compile(r"active"))
    page.wait_for_timeout(1000) 
    # 确认删除并拦截 DELETE 请求
    with page.expect_response(f"**/api/manage-videos?videoId={new_video_id}") as del_resp:
        page.locator("#delete-confirm-btn").click()
        
    assert del_resp.value.request.method == "DELETE"
    assert del_resp.value.ok
    
    # 终极断言:这个 DOM 节点已经彻底消失,不仅保证了 UI 的正确性,也确保了下次跑脚本时数据库是干净的
    expect(target_row).not_to_be_visible()
    page.wait_for_timeout(1000) # 视觉停顿
    print(f"🎉 视频 CRUD 闭环测试完美通过!测试数据 (ID:{new_video_id}) 已安全销毁。")



# 你提供的新物理测试物料
NEW_COVER_PATH = r"D:\temp_video\covers\16_faluo.jpg"
NEW_VIDEO_PATH = r"D:\temp_video\16_faluoqundao.mp4"
#第四个函数:上传后修改:删除旧标签、增加新标签、替换封面和视频源文件,最后再删除这个视频,保持环境干净
def test_b_end_video_complex_update(page: Page):
    """
    测试复杂修改:删除标签、增加标签、替换封面和视频源文件
    """
    # 确保新文件存在
    assert os.path.exists(NEW_COVER_PATH), f"❌ 找不到新封面图: {NEW_COVER_PATH}"
    assert os.path.exists(NEW_VIDEO_PATH), f"❌ 找不到新视频: {NEW_VIDEO_PATH}"

    print("\n[Step 1] 极速造数据:先上传一个带初始标签的临时视频...")
    page.goto(f"{BASE_URL}/management.html")
    
    # 造一个基础视频
    page.locator("#upload-title").fill("临时视频-准备被修改")
    page.locator("#upload-description").fill("临时视频-描述")
    page.locator("#upload-tag-container .tag-input-field").fill("旧标签待删除")
    page.locator("#upload-tag-container .tag-input-field").press("Enter")
    # 使用你之前的封面和视频作为初始文件
    page.locator("#upload-cover-input").set_input_files(COVER_PATH) 
    page.locator("#upload-video-input").set_input_files(VIDEO_PATH)
    page.wait_for_timeout(1000) 

    with page.expect_response("**/api/manage-videos", timeout=60000) as upload_resp:
        page.locator("#upload-submit-btn").click()
    page.wait_for_timeout(1000)
    
    new_video_id = upload_resp.value.json().get("video", {}).get("newVideoId")
    page.locator("#alert-modal-ok-btn").click()
    page.wait_for_timeout(1000)

    # ---------------------------------------------------------
    # 🎬 核心测试开始:复杂的修改交互
    # ---------------------------------------------------------
    print("[Step 2] 切换至管理面板,打开编辑模态框...")
    with page.expect_response("**/api/manage-videos*page=1*"):
        page.locator('.nav-link[data-tab="management"]').click()
    page.wait_for_timeout(1500)
    target_row = page.locator(f'tr[data-video-id="{new_video_id}"]')
    target_row.locator(".btn-action-edit").click()
    expect(page.locator("#edit-modal")).to_have_class(re.compile(r"active"))
    page.wait_for_timeout(1000)

    print("[Step 3] 执行复杂修改:操作标签与替换物理文件...")
    # 1. 删除旧标签:定位到编辑框里的第一个标签,并点击它的 '×' 按钮
    first_tag_remove_btn = page.locator("#edit-tag-container .tag-chip").first.locator(".tag-remove-btn")
    first_tag_remove_btn.click()
    page.wait_for_timeout(1000)

    # 2. 增加新标签
    page.locator("#edit-tag-container .tag-input-field").fill("法罗群岛")
    page.locator("#edit-tag-container .tag-input-field").press("Enter")
    
    
    # 3. 挂载新的封面和视频文件
    page.locator("#edit-cover-input").set_input_files(NEW_COVER_PATH)
    page.locator("#edit-video-input").set_input_files(NEW_VIDEO_PATH)
    page.wait_for_timeout(1000)
    
    print("  -> 保存复杂修改,等待 COS 上传新文件并回滚旧文件...")
    # 拦截 PUT 请求,因为涉及新文件上传到腾讯云,给足超时时间
    with page.expect_response("**/api/manage-videos", timeout=60000) as edit_resp:
        page.locator("#edit-save-btn").click()
        
    assert edit_resp.value.ok, "更新接口报错!"
    
    # 验证 UI 上的标签是否更新(“旧标签待删除”应消失,“法罗群岛”应出现)
    expect(target_row.locator(".tag-chip-container")).not_to_contain_text("旧标签待删除")
    expect(target_row.locator(".tag-chip-container")).to_contain_text("法罗群岛")
    print("✅ 复杂修改测试通过:标签增删及大文件替换均成功!")

    # ---------------------------------------------------------
    # 🎬 扫尾清理
    # ---------------------------------------------------------
    print("[Step 4] 测试扫尾:删除该视频...")
    target_row.locator(".btn-action-delete").click()
    page.wait_for_timeout(1000)
    with page.expect_response(f"**/api/manage-videos?videoId={new_video_id}"):
        page.locator("#delete-confirm-btn").click()
    page.wait_for_timeout(1000)
    print("✅ 测试数据彻底清理完毕!")


# pytest test_c_end_journey.py --headed -s
# pytest test_c_end_journey.py --headed -s --alluredir=./result_1
# allure serve ./result_1 # 启动 Allure 报告面板
# pytest test_c_end_journey.py -k "audit_and_cleanup" --headed -s
# pytest test_c_end_journey.py -k "complex_update" --headed -s

1.2.2 Allure 生成测试报告
PS F:\my-video-app> pytest test_c_end_journey.py --headed -s --alluredir=./result_1
=============================================================== test session starts ================================================================
platform win32 -- Python 3.7.8, pytest-7.4.4, pluggy-1.2.0
rootdir: F:\my-video-app
plugins: allure-pytest-2.15.3, anyio-3.7.1, base-url-2.0.0, html-3.2.0, metadata-3.0.0, playwright-0.3.3
collected 4 items

test_c_end_journey.py 
[Step 1] 访问首页,等待视频列表加载...
[Step 2] 模拟用户输入搜索词并搜索...
[Step 3] 点击视频,验证跳转及详情加载...
[Step 4] 严格验证播放页 DOM 渲染...
[Step 5] 模拟用户输入带时间戳的评论并发布...
✅ 用户核心体验链路测试通过,丝滑且清晰!
.
[Step 1] 访问管理后台,切换至评论管理...
[Step 2] 搜索带有“时间戳”关键字的测试评论...
[Step 3] 开始循环清理测试脏数据...
  -> 当前列表还有 1 条测试评论,准备删除第一条...
✅ 所有“时间戳”相关的测试评论已清理完毕!
[Step 4] 终极断言:验证表格内已无相关数据...
🎉 B端评论审核与管控链路测试通过!数据库干干净净!
.
[Step 1] 访问管理后台,准备执行物理文件上传...
[Step 2] 提交上传,正在等待 COS 与数据库响应 (耗时较长,请耐心等待)...
✅ 上传成功!成功捕获新视频 ID: 32
[Step 3] 切换至视频管理面板,执行数据修改...
  -> 保存修改信息...
✅ 视频信息修改成功,UI 状态已同步更新!
[Step 4] 执行测试扫尾:彻底删除该视频与关联文件...
🎉 视频 CRUD 闭环测试完美通过!测试数据 (ID:32) 已安全销毁。
.
[Step 1] 极速造数据:先上传一个带初始标签的临时视频...
[Step 2] 切换至管理面板,打开编辑模态框...
[Step 3] 执行复杂修改:操作标签与替换物理文件...
  -> 保存复杂修改,等待 COS 上传新文件并回滚旧文件...
✅ 复杂修改测试通过:标签增删及大文件替换均成功!
[Step 4] 测试扫尾:删除该视频...
✅ 测试数据彻底清理完毕!
.

=========================================================== 4 passed in 85.86s (0:01:25) =========================================================== 


启动 Allure 报告面板
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


2 测试用例

2.1 主页视频列表展示与搜索

前置条件

  1. 确保本地 vercel dev 服务正常运行,或使用已配置好的 Vercel 线上域名。

  2. PostgreSQL 数据库 videos表 、tags表 、video_tags 表与 comments 表连接正常。

  3. 腾讯云 COS 状态正常。

测试步骤

步骤操作预期结果
1浏览器访问 /index.html页面正常加载,顶部展示导航栏与搜索框。
2查看核心主区域成功渲染「精选视频」列表,每个视频卡片均包含:封面图、视频标题、上传日期、以及动态拉取的真实播放量
3在顶部搜索框输入现有视频的标题或标签关键字,点击搜索图标页面不发生全页刷新,标题变为 搜索: "关键字",视频网格中仅展示标题或标签匹配的视频卡片。
4点击任意视频卡片触发底层播放量统计接口,并成功携带 id 参数跳转至 player.html

预期结果

模块具体成果详情
数据库 (videos 表)对应视频的views_count + 1

2.2 视频播放防盗链与评论交互

前置条件

  1. 访问 /index.html页面

测试步骤

步骤操作预期结果
1从主页点击视频,进入 /player.html?id=xxx页面正确获取视频详情,原生的播放器挂载成功并开始自动播放。
2查看控制台网络请求数据库接口返回的视频播放地址 signed_play_url 包含了 MD5 动态生成的 sign 签名和 t(时间戳),有效验证了 CDN 防盗链机制。
3在下方评论区输入一条带时间戳的测试评论并点击“发布评论”评论框清空,“发布评论”按钮解除禁用状态。
4观察评论列表区无需刷新整个页面,新发布的评论直接显示在评论列表最顶部。

预期结果

模块具体成果详情
数据库 (videos 表)对应视频idviews_count + 1
数据库 (comments 表)新增 1 条数据:comment_id 递增;video_id 为该视频 idcontent 初始化为带时间戳的测试评论;created_at 为当前时间

2.3 管理员页面:视频上传(Create)

前置条件

  1. 访问 /management.html

  2. 本地准备好符合格式规范的测试物料(如 .jpg 封面,.mp4 视频)。

测试步骤

步骤操作预期结果
1点击左侧导航栏的「视频上传」右侧展示上传表单,包含标题、描述、标签输入框及文件上传区域。
2填写标题与描述,在标签框输入 童年生活 并敲击回车标签框内正确生成带有 × 按钮的蓝色交互式 Tag Chip 元素。
3分别选择本地的封面图与视频文件封面上传区域成功渲染本地图片的预览缩略图。
4点击「提交上传」按钮按钮变为“上传中…”,显示上传进度条。向腾讯云请求临时 STS 密钥并直传文件。
5等待进度条到达 100%弹出「上传成功」全局确认框,表单自动重置清空。

预期结果

模块具体成果详情
数据库 (videos 表)新增 1 条数据:id 递增;cover_key 记录为 covers/{文件名}video_key 记录为 videos/{文件名}views_count 初始化为 0upload_date 为当前上传时间。
数据库 (tags 表)若标签不存在则新建并生成 tag_id,若存在则复用现有 tag_id。保证全库标签唯一。
数据库 (video_tags 表)新增映射记录,完成当前 video_id 与对应 tag_id 的多对多绑定。
云存储 (腾讯云 COS)video-321 存储桶内,covers/videos/ 目录下成功生成对应的物理文件。

2.4 管理员页面:视频信息修改(Update)

前置条件

  1. 访问 /management.html,进入「视频管理」。

测试步骤

步骤操作预期结果
1在列表中找到目标视频,点击操作列的「编辑」按钮弹出 #edit-modal 编辑模态框,并自动回显该视频的所有原始信息(标题、描述、旧标签、旧封面预览图)。
2修改视频标题,删除一个旧标签,新增一个测试标签文本与标签组件 UI 状态实时同步更新。
3点击「上传新封面」「上传新视频」选择一个全新的封面和视频文件封面上传区域成功渲染本地图片的预览缩略图,触发文件替换逻辑的预备状态。
4点击模态框底部的「保存更改」后台开始上传新文件至 COS,写入数据库,并触发旧文件的清理机制。
5等待请求完成模态框自动关闭,页面顶部提示绿色「更新成功」,表格内当前行的标题与标签数据实时刷新。

预期结果

模块具体成果详情
数据库 (videos 表)对应记录的标题、描述更新;cover_key 记录变为 covers/{新文件名}video_key 记录变为 videos/{新文件名}upload_date 被更新为当前修改时间 。
数据库 (tags 表)若标签不存在则新建并生成 tag_id,若存在则复用现有 tag_id。保证全库标签唯一。
数据库 (video_tags 表)删除旧的映射记录;新增映射记录,完成当前 video_id 与对应 tag_id 的多对多绑定。
云存储 (腾讯云 COS)video-321 存储桶内,covers/videos/ 目录下成功生成对应的物理文件。;同时旧的物理文件被彻底删除,防止云端空间浪费。

2.5 管理员页面:视频删除(Delete)

前置条件

  1. 访问 /management.html,进入「视频管理」。

测试步骤

步骤操作预期结果
1在「视频管理」中,搜索待删除的视频标题列表过滤,仅展示匹配项。
2点击目标视频操作列的「删除」按钮弹出 #delete-confirm-modal 红色预警弹窗,提示操作不可逆。
3点击「确认删除」按钮弹窗关闭,列表展示「删除中…」并随后提示成功。
4等待请求完成表格内无当前行的标题与标签数据。

预期结果

模块具体成果详情
数据库 (videos 表)videos 表对应该视频id的所有信息被删除。
数据库 (video_tags 表)视频id对应 video_tags 表中的所有标签关联记录被删除。
数据库 (comments 表)视频id该对应 comments 表下的所有评论数据被被删除。
云存储 (腾讯云 COS)腾讯云 COS 中关联的封面图片和视频文件被双双抹除。

2.6 管理员页面:评论审核与清理

前置条件

  1. 访问 /management.html

测试步骤

步骤操作预期结果
1切换「评论管理」渲染全站评论表格,包含评论内容、所属视频标题、发表时间。
2使用双重条件检索:在搜索框分别填入特定的“评论关键字”和“视频标题”底层触发带有联合查询的 SQL,表格精准筛选出匹配的评论。
3点击评论的「删除」按钮,在弹窗中确认接口调用成功后,该评论从列表中消失。对应播放页不再显示该条评论。

预期结果

模块具体成果详情
数据库 (comments 表)对应的评论id数据被被删除。

2.7 管理员页面:统计分析大屏

前置条件

  1. 访问 /management.html

测试步骤

步骤操作预期结果
1切换至「统计分析」页面出现三个模块的 正在加载... 提示。
2观察数据渲染完毕后的展示三个卡片分别正确渲染:

1. 播放量 Top 5 视频排行。

2. 价值 Top 5 标签。

3. 热门 Top 5 标签。

3 解决BUG

  1. 执行测试用例15的时候,评论页面插入一个2000个字的评论,导致播放页面被拉长,比例不协调。在播放页面和后台页面针对50字以上的评论加展开收回功能,保证了观看体验;
  1. manage-video.js的POST提交请求,返回状态码修改为201,而不是200;
  1. manage-comment.js的99行代码修改为:if (result.length === 0),判断删除后是否有返回,判断长度用length而不是rowCount;
  1. 用例19针对不存在的id返回404,用例20-22针对负数、字母和空的id返回400。优化update_view.js逻辑;
  1. 用例8-10针对负数、字母和空的id返回400(之前未做判断,导致返回500,不合理),优化get_video_details.js逻辑;
  1. 用例35-37针对负数、字母和空的评论id返回400。优化manage-comments.js的DELETE逻辑;
  1. 限制page和limit必须为正数,且limit被限制最大为10(避免一次加载太多卡顿)。优化manage-videos.js的GET逻辑;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值