mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(engine): measure the video's box before styling its replacement frame
injectVideoFramesBatch measured the video's used box (offsetLeft/Top/ Width/Height) after creating, inserting, and styling the replacement <img> sibling -- once that copy loop started carrying border-width (added for the border-visibility fix), the freshly-inserted, still in-flow, now-bordered <img> competed for space in a flex row before the video's own box was ever read. In a flex-centered layout (video width:100%, sharing a row with the img sibling), that shrank the measured video box by the exact width of the border, so the img ended up sized from the video's post-shrink box instead of its authored one. Moved the measurement to before the <img> is created/inserted/styled at all, and set img.style.boxSizing = "border-box" explicitly after the style-copy loop, since offsetWidth/offsetHeight (and the getBoundingClientRect fallback) are always a border-box measurement regardless of what box-sizing value that loop copies from the video's own computed style. Added a flex-centered regression test asserting the replacement img's box stays identical to the video's box; confirmed it fails with the old measurement order (a 16px border shrinks a 500px box to 484px) and passes with the fix. Regenerated the one fixture (style-9-prod) whose committed golden had baked in the shrunk-frame geometry. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
This commit is contained in:
co-authored by
Miguel Ángel
parent
ecf073fb04
commit
c888903579
@@ -744,6 +744,21 @@ export async function injectVideoFramesBatch(
|
||||
? 1
|
||||
: opacityParsed;
|
||||
|
||||
// Measure the video's used box BEFORE creating/inserting/styling its
|
||||
// replacement <img> sibling. A freshly-created sibling starts out
|
||||
// in-flow (position:absolute is only applied below, once this
|
||||
// measurement is done) and, now that MEDIA_VISUAL_STYLE_PROPERTIES
|
||||
// includes border-width, carries a border as soon as the copy loop
|
||||
// below runs -- in a flex layout that in-flow bordered sibling
|
||||
// shrinks the video's own flex box, so measuring afterward reads the
|
||||
// video's shrunk-by-its-own-replacement box instead of its authored
|
||||
// one.
|
||||
const videoRect = video.getBoundingClientRect();
|
||||
const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
|
||||
const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
|
||||
const offsetWidth = video.offsetWidth > 0 ? video.offsetWidth : videoRect.width;
|
||||
const offsetHeight = video.offsetHeight > 0 ? video.offsetHeight : videoRect.height;
|
||||
|
||||
if (isNewImage) {
|
||||
img = document.createElement("img");
|
||||
img.classList.add("__render_frame__");
|
||||
@@ -780,23 +795,24 @@ export async function injectVideoFramesBatch(
|
||||
// stack vertically — the <img> lands below the video and gets clipped
|
||||
// by any overflow:hidden ancestor (e.g., border-radius wrappers).
|
||||
//
|
||||
// Apply this after visual style copying so the measured used box is
|
||||
// the final authority for replacement frame geometry.
|
||||
{
|
||||
const videoRect = video.getBoundingClientRect();
|
||||
const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
|
||||
const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
|
||||
const offsetWidth = video.offsetWidth > 0 ? video.offsetWidth : videoRect.width;
|
||||
const offsetHeight = video.offsetHeight > 0 ? video.offsetHeight : videoRect.height;
|
||||
img.style.position = "absolute";
|
||||
img.style.inset = "auto";
|
||||
img.style.left = `${offsetLeft}px`;
|
||||
img.style.top = `${offsetTop}px`;
|
||||
img.style.right = "auto";
|
||||
img.style.bottom = "auto";
|
||||
img.style.width = `${offsetWidth}px`;
|
||||
img.style.height = `${offsetHeight}px`;
|
||||
}
|
||||
// The used box was captured above, before this <img> was
|
||||
// created/inserted/styled, so it reflects the video's own authored
|
||||
// layout rather than a box already perturbed by its replacement.
|
||||
img.style.position = "absolute";
|
||||
img.style.inset = "auto";
|
||||
img.style.left = `${offsetLeft}px`;
|
||||
img.style.top = `${offsetTop}px`;
|
||||
img.style.right = "auto";
|
||||
img.style.bottom = "auto";
|
||||
img.style.width = `${offsetWidth}px`;
|
||||
img.style.height = `${offsetHeight}px`;
|
||||
// offsetWidth/offsetHeight (and the getBoundingClientRect fallback)
|
||||
// are always the video's border-box, so the <img> must also interpret
|
||||
// its own width/height as border-box -- regardless of what box-sizing
|
||||
// value the copy loop above just copied from the video's own computed
|
||||
// style (which could be content-box), since that would otherwise size
|
||||
// the <img>'s content area past the measured box by its own border.
|
||||
img.style.boxSizing = "border-box";
|
||||
img.style.objectFit = computedStyle.objectFit;
|
||||
img.style.objectPosition = computedStyle.objectPosition;
|
||||
img.style.zIndex = computedStyle.zIndex;
|
||||
|
||||
@@ -129,4 +129,63 @@ describe("video border/border-radius/clip-path on the replacement render frame",
|
||||
expectBlueVideoContent(await readPixel(page, screenshot, VIDEO_CENTER));
|
||||
expectWhitePageBackground(await readPixel(page, screenshot, CLIPPED_CORNER));
|
||||
});
|
||||
|
||||
// Regression case: adding border-width to the copy allow-list can change the
|
||||
// replacement <img>'s LAYOUT, not just its paint. Before the video is
|
||||
// measured, the freshly-created (or freshly re-styled) <img> sibling is
|
||||
// briefly an in-flow flex item -- once it carries a border, it competes for
|
||||
// space in the flex row and shrinks the video's own box, so a naive
|
||||
// "measure after styling" order bakes in the wrong (shrunk) geometry. This
|
||||
// exact layout shape (flex row, centered, video width:100%, global
|
||||
// border-box reset) is what a real fixture (style-9-prod) hit in CI; the
|
||||
// pixel-probe tests above use position:absolute layouts that were never
|
||||
// exposed to this bug, so they can't catch a regression here.
|
||||
it("keeps the replacement <img>'s box identical to the video's own box in a flex-centered layout", async () => {
|
||||
await page.setViewport({ width: 500, height: 300 });
|
||||
await page.setContent(`<!doctype html>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; background: #ffffff; }
|
||||
#aroll-container {
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
width: 500px; height: 300px;
|
||||
}
|
||||
</style>
|
||||
<div id="aroll-container">
|
||||
<video
|
||||
id="${VIDEO_ID}"
|
||||
class="clip"
|
||||
muted
|
||||
style="width:100%;height:150px;object-fit:fill;
|
||||
border:${BORDER_WIDTH}px solid red;border-radius:${CORNER_RADIUS}px;"
|
||||
></video>
|
||||
</div>`);
|
||||
|
||||
await injectVideoFramesBatch(page, [{ videoId: VIDEO_ID, dataUri: BLUE_PIXEL_DATA_URI }]);
|
||||
|
||||
const { videoBox, imgBox } = await page.evaluate((videoId) => {
|
||||
const video = document.getElementById(videoId) as HTMLVideoElement;
|
||||
const img = video.nextElementSibling as HTMLImageElement;
|
||||
const roundedRect = (rect: DOMRect) => ({
|
||||
left: Math.round(rect.left),
|
||||
top: Math.round(rect.top),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
});
|
||||
return {
|
||||
videoBox: roundedRect(video.getBoundingClientRect()),
|
||||
imgBox: roundedRect(img.getBoundingClientRect()),
|
||||
};
|
||||
}, VIDEO_ID);
|
||||
|
||||
expect(imgBox).toEqual(videoBox);
|
||||
// Sanity-check the fixture itself reaches its full authored width: the
|
||||
// video's own box (read after injection, once the sibling <img> is
|
||||
// position:absolute and back out of flow) is unaffected by this bug
|
||||
// either way, so this only confirms the flex row had room to shrink into
|
||||
// in the first place -- the actual regression is caught by the
|
||||
// `imgBox`/`videoBox` comparison above, since the <img>'s width was baked
|
||||
// in earlier, while it was still an in-flow, bordered flex sibling.
|
||||
expect(videoBox.width).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b50f12be665ba9ac02cb9a11e3530794123d3712721c13c59197d853b3cc6c18
|
||||
size 13629844
|
||||
oid sha256:586aef50c55b7cc03d02b5c2f4200fd0a13453f1d09fbfe1ab221b0c71b88336
|
||||
size 13670413
|
||||
|
||||
Reference in New Issue
Block a user