"""web/assets/crop.js 的模块级测试(数据 → 测试过程 → 验证结果)。 被测模块:`web/assets/crop.js`(前端框选矩形 ↔ crop 比例的纯函数,含 object-fit: contain 的 letterbox 处理),被 OCR 框选面板复用。 实现方式:在 Node.js 子进程中加载真实 JS 模块并调用真实函数(不重写逻辑), 验证换算结果;环境无 node 时跳过(保持跨平台可运行)。 """ from __future__ import annotations import json import shutil import subprocess from pathlib import Path import pytest # 仓库根与被测脚本。 WORKSPACE = Path(__file__).resolve().parents[3] CROP_JS = WORKSPACE / "web" / "assets" / "crop.js" # 用真实 JS 引擎执行调用的封装:把用例数据交给 crop.js 的真实实现。 _CALL_SCRIPT = """ const crop = require(process.argv[1]); const payload = JSON.parse(process.argv[2]); const fn = crop[payload.fn]; const result = fn(...payload.args); process.stdout.write(JSON.stringify(result)); """ def _run(fn: str, *args) -> object: """在 node 中调用 crop.js 的真实函数并返回解析后的结果。""" node = shutil.which("node") if node is None: pytest.skip("环境没有 node,跳过 JS 模块测试") completed = subprocess.run( [node, "-e", _CALL_SCRIPT, str(CROP_JS), json.dumps({"fn": fn, "args": list(args)})], capture_output=True, text=True, ) assert completed.returncode == 0, completed.stderr return json.loads(completed.stdout) # --------------------------------------------------------------------------- # videoDisplayRect:contain 显示区域 # --------------------------------------------------------------------------- def test_display_rect_matches_box_when_aspect_equal() -> None: """视频与容器宽高比一致时铺满容器,无留边。""" # 数据:1920x1080 视频放进 960x540 容器。 # 测试过程 rect = _run("videoDisplayRect", 1920, 1080, 960, 540) # 验证结果 assert rect == {"x": 0, "y": 0, "w": 960, "h": 540} def test_display_rect_letterboxes_on_wide_container() -> None: """容器比视频更宽时左右留边(横屏视频放进方形容器)。""" # 数据:1920x1080 视频放进 1000x1000 容器。 # 测试过程 rect = _run("videoDisplayRect", 1920, 1080, 1000, 1000) # 验证结果:宽 1000、高 562.5,上下居中留边。 assert rect["w"] == pytest.approx(1000) assert rect["h"] == pytest.approx(562.5) assert rect["x"] == pytest.approx(0) assert rect["y"] == pytest.approx((1000 - 562.5) / 2) def test_display_rect_letterboxes_on_tall_container() -> None: """容器比视频更高时上下留边。""" # 数据:1000x1000 视频放进 500x1000 容器。 # 测试过程 rect = _run("videoDisplayRect", 1000, 1000, 500, 1000) # 验证结果:宽高均为 500,水平居中。 assert rect["w"] == pytest.approx(500) assert rect["h"] == pytest.approx(500) assert rect["x"] == pytest.approx(0) assert rect["y"] == pytest.approx(250) # --------------------------------------------------------------------------- # rectToCrop:框选 → 比例 # --------------------------------------------------------------------------- def test_rect_to_crop_full_frame() -> None: """框选整个显示区域得到 [0,0,1,1]。""" # 数据:显示区为整个容器。 # 测试过程 crop = _run("rectToCrop", {"x": 0, "y": 0, "w": 960, "h": 540}, 1920, 1080, 960, 540) # 验证结果 assert crop == [0, 0, 1, 1] def test_rect_to_crop_bottom_quarter() -> None: """框选底部 1/4 得到默认字幕区比例 [0,0.75,1,0.25]。""" # 数据:底部四分之一的显示坐标。 # 测试过程 crop = _run("rectToCrop", {"x": 0, "y": 405, "w": 960, "h": 135}, 1920, 1080, 960, 540) # 验证结果 assert crop == [0, 0.75, 1, 0.25] def test_rect_to_crop_accounts_for_letterbox_offset() -> None: """存在留边时,框选坐标先减去显示区偏移(否则比例整体偏移)。""" # 数据:1920x1080 放 1000x1000 容器(上下各留 218.75),框选显示区上半。 rect = {"x": 0, "y": 218.75, "w": 1000, "h": 281.25} # 测试过程 crop = _run("rectToCrop", rect, 1920, 1080, 1000, 1000) # 验证结果:y 从 0 开始、高度占一半。 assert crop[0] == pytest.approx(0) assert crop[1] == pytest.approx(0) assert crop[2] == pytest.approx(1) assert crop[3] == pytest.approx(0.5) def test_rect_to_crop_clamps_out_of_range_to_0_1() -> None: """越界框选被钳制到 0~1(不产生非法 crop 参数)。""" # 数据:超出显示区的矩形(含负坐标)。 # 测试过程 crop = _run("rectToCrop", {"x": -500, "y": -500, "w": 5000, "h": 5000}, 1920, 1080, 960, 540) # 验证结果:全部落在 [0,1]。 assert all(0 <= value <= 1 for value in crop) assert crop[0] == 0 and crop[1] == 0 assert crop[2] == 1 and crop[3] == 1 def test_rect_to_crop_rounds_to_three_decimals() -> None: """比例保留 3 位小数(与帧清单时间戳精度约定一致)。""" # 数据:会产生长小数的框选。 # 测试过程 crop = _run("rectToCrop", {"x": 1, "y": 2, "w": 333, "h": 77}, 1920, 1080, 960, 540) # 验证结果:每个值最多 3 位小数。 for value in crop: assert round(value, 3) == value # --------------------------------------------------------------------------- # cropToRect:比例 → 框选(回显) # --------------------------------------------------------------------------- def test_crop_to_rect_is_inverse_of_rect_to_crop() -> None: """比例转回矩形与原始框选一致(回显正确,误差在 3 位小数内)。""" # 数据:一个中间框选。 rect = {"x": 120, "y": 300, "w": 480, "h": 135} # 测试过程 crop = _run("rectToCrop", rect, 1920, 1080, 960, 540) restored = _run("cropToRect", crop, 1920, 1080, 960, 540) # 验证结果:各分量误差极小。 for key in ("x", "y", "w", "h"): assert restored[key] == pytest.approx(rect[key], abs=1.0) def test_crop_to_rect_bottom_quarter() -> None: """默认 crop 比例回显为底部四分之一矩形。""" # 数据:默认 crop。 # 测试过程 rect = _run("cropToRect", [0, 0.75, 1, 0.25], 1920, 1080, 960, 540) # 验证结果 assert rect["x"] == pytest.approx(0) assert rect["y"] == pytest.approx(405) assert rect["w"] == pytest.approx(960) assert rect["h"] == pytest.approx(135)