From 326fe24cbace06e2e9abfd6f49c009a6a26c06ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 08:23:16 -0400 Subject: [PATCH] Video: the check compares both renderers at the cut and every exit frame's recordings (R2.3) --- agents.md | 2 +- site/video/scenes/verify.mjs | 58 ++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/agents.md b/agents.md index 7828697..a9929fb 100644 --- a/agents.md +++ b/agents.md @@ -173,7 +173,7 @@ tests/scripts/ooxml.py <用例> <文件> 不经 LibreOffice 读 DOCX、XLSX、P tests/scripts/webview.sh 真窗口 CEF 页面:输入法组字与提交、键入、焦点、主题变量、缩放、不可离开所在文件夹 cd site && pnpm deploy 官网构建并经 wrangler 发到 Cloudflare Workers(只有静态资源),地址 https://ely-office.zhangyanghaha0407.workers.dev cd site && pnpm test && pnpm e2e 官网单测(几何、道路连通)与 Playwright 端到端(悬停、聚焦、按压、命中归属) -cd site/video/scenes && npm run verify 发布视频:离场六块屏幕在切场后各播自己的录屏帧(真页面渲染) +cd site/video/scenes && npm run verify 发布视频:群像与离场两个渲染器在切场处屏幕角点一致,离场全程各屏播自己的录屏帧(真页面渲染) cargo check --workspace cargo test --workspace rm -rf target/debug/incremental 每次构建或测试后都删(用户定:增量缓存会占满磁盘) diff --git a/site/video/scenes/verify.mjs b/site/video/scenes/verify.mjs index ba71b14..a8ddd8a 100644 --- a/site/video/scenes/verify.mjs +++ b/site/video/scenes/verify.mjs @@ -1,33 +1,67 @@ -// Checks the leaving screens play their recordings at the cut. +// Checks the cut into the exit: same screens, their own recordings. import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { chromium } from "playwright"; import { createServer } from "vite"; import recs from "../montage/recs.json" with { type: "json" }; +import { moment } from "./timeline.ts"; +import { recording } from "./turn.ts"; const here = dirname(fileURLToPath(import.meta.url)); const server = await createServer({ root: resolve(here, "../.."), configFile: false, logLevel: "error", server: { port: 0 } }); await server.listen(); +const base = server.resolvedUrls.local[0]; const browser = await chromium.launch(); -try { + +/** A film-sized page whose recordings are any one frame. */ +async function open(path) { const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); page.on("pageerror", (e) => { throw e; }); - // Any frame will do; only which frame is asked matters. await page.route("**/rec/**", (route) => route.fulfill({ path: resolve(here, "../../public/film/poster.jpg") })); - await page.goto(`${server.resolvedUrls.local[0]}video/scenes/index.html`, { waitUntil: "networkidle" }); - // The first frame after the montage is the cut. + await page.goto(`${base}${path}`, { waitUntil: "networkidle" }); + return page; +} + +/** Each drawn recording: its source and three screen corners. */ +const drawn = (page) => + page.$$eval("#world image", (els) => + els.map((el) => { + const m = el.getScreenCTM(); + const at = (x, y) => [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f]; + return { href: el.getAttribute("href"), corners: [at(0, 0), at(1280, 0), at(0, 720)] }; + }), + ); + +try { const cut = recs.first + 129; - for (const f of [cut, cut + 1, cut + 2]) { - await page.evaluate((f) => window.ely.render("files", f), f); - const hrefs = await page.$$eval("#world image", (els) => els.map((el) => el.getAttribute("href"))); - const want = String(f - recs.first + 1).padStart(5, "0"); - if (hrefs.length !== 6 || !hrefs.every((h) => h.endsWith(`/${want}.jpg`)) || new Set(hrefs).size !== 6) { - throw new Error(`frame ${f}: ${hrefs.length} recordings ${JSON.stringify(hrefs)}`); + const montage = await open("video/montage/index.html"); + await montage.waitForFunction(() => window.montage); + const { pins } = await montage.evaluate((f) => window.montage.render(f), cut - 1); + const exit = await open("video/scenes/index.html"); + // Every exit frame until the last screen has gone. + for (let f = cut; ; f++) { + await exit.evaluate((f) => window.ely.render("files", f), f); + const shown = await drawn(exit); + const want = moment(f / 30).pieces.map(({ box, land }) => recording(box, land, f)).filter(Boolean); + const got = shown.map((s) => s.href); + if (JSON.stringify([...got].sort()) !== JSON.stringify([...want].sort())) { + throw new Error(`frame ${f}: drew ${JSON.stringify(got)}, not ${JSON.stringify(want)}`); } + if (f === cut) { + if (got.length !== 6) throw new Error(`the cut shows ${got.length} screens`); + // The montage pins top-left, top-right, bottom-right, bottom-left. + for (const { href, corners } of shown) { + const app = recs.apps.find((a) => href.startsWith(`rec/${a.rec}/`)).app; + const pin = pins[app]; + const near = [pin[0], pin[1], pin[3]].every((p, i) => Math.hypot(p[0] - corners[i][0], p[1] - corners[i][1]) < 1); + if (!near) throw new Error(`${app} moves at the cut: ${JSON.stringify(pin)} to ${JSON.stringify(corners)}`); + } + } + if (!want.length) break; } - console.log("verify: six screens play their own frames across the cut"); + console.log("verify: screens hold still across the cut and play their own frames"); } finally { await browser.close(); await server.close();