mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(engine): copy border-width/style/color onto the replacement video frame (#3993)
* fix(engine): copy border-width/style/color onto the replacement video frame
injectVideoFramesBatch substitutes each <video> with a sibling <img> holding
an extracted still frame, copying an allow-listed set of CSS properties from
the video's computed style onto the img. border-width/border-style/
border-color were absent from that allow-list, so any border authored
directly on a <video> never reached the replacement image and disappeared
from render/snapshot output. border-radius and clip-path were already on the
list and already clip a replaced element's content correctly without
needing overflow:hidden, confirmed by a real-Chromium test that exercises
the actual capture path and reads real screenshot pixels.
Also corrected a stale comment in frameCapture.ts claiming
detectCssEffectRisk can return "clip-path" as a risk value -- it can't; that
string only ever comes from a separate, animation-only at-risk-props gate
further down the same file.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* test(producer): regenerate style-9-prod golden for the border fix
style-9-prod's video has an authored 8px border + 16px border-radius
(design_review.md calls out the "floating" look) that never rendered before
this fix, so its committed golden baked in the pre-fix (borderless) output.
Regenerated via `tsx src/regression-harness.ts style-9-prod --update`;
verified the diff is exactly the border/radius becoming visible (extracted
frames before/after) and the fixture passes its own visual/audio checks
again (100/100 checkpoints, correlation 1.000).
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* 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>
* refactor(engine): consolidate video-box measurement into one object
Follow-up to c88890357: group the four measurement locals into a
single videoBox object instead of four loose consts, and trim the
surrounding comments down to their load-bearing content. No behavior
change -- same measurement order, same fallback conditions, same
applied style values; re-verified red (reverting the measurement order
still reproduces the 500px -> 484px shrink) and green.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(engine): trim the border-fix comments to the invariant
* test(producer): keep main's style-9-prod compiled.html; timings are unchanged
* test(producer): repin the classification source hash for test reachability
* fix(core): a color-graded bordered source keeps its canvas inside its border box
* fix(core): a graded source with an empty content box still shows its border
* fix(core): a border-only graded canvas stays hidden until the source has drawn
* fix(core): border-only graded canvas shows only while grading hides the source
* test(core): pin the border-only canvas hide after a context loss
* fix(core): grading canvas carries no border, so a graded source never paints it twice
* fix(core): graded injected frame drops its border once grading draws over it
* fix(core): a released grading entry gives the injected frame its border back
* fix(core): a released injected frame takes the video's current border style
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
This commit is contained in:
co-authored by
Miguel Ángel
parent
bb9e507e84
commit
e23b3a3e31
@@ -14,6 +14,9 @@ export const MEDIA_VISUAL_STYLE_PROPERTIES = [
|
||||
"filter",
|
||||
"mix-blend-mode",
|
||||
"backdrop-filter",
|
||||
"border-width",
|
||||
"border-style",
|
||||
"border-color",
|
||||
"border-radius",
|
||||
"overflow",
|
||||
"clip-path",
|
||||
|
||||
@@ -237,6 +237,18 @@ describe("createColorGradingRuntime", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("draws a bordered source's grading canvas without a border over its whole box", () => {
|
||||
vi.stubGlobal("devicePixelRatio", 1);
|
||||
const video = makeDrawableVideo();
|
||||
video.style.border = "16px solid rgb(255, 0, 0)";
|
||||
|
||||
const { canvas } = startRuntimeWithVideo(video);
|
||||
|
||||
expect(canvas.style.borderStyle).toBe("none");
|
||||
expect(canvas.width).toBe(640);
|
||||
expect(canvas.height).toBe(360);
|
||||
});
|
||||
|
||||
it("says an element is graded only while it draws through a grading canvas", () => {
|
||||
const { video } = startRuntimeWithVideo();
|
||||
const plain = document.createElement("video");
|
||||
@@ -875,6 +887,40 @@ describe("createColorGradingRuntime", () => {
|
||||
expect(canvas.style.opacity).toBe("0.75");
|
||||
});
|
||||
|
||||
it("drops the injected render frame's border while grading draws over it", () => {
|
||||
const video = makeDrawableVideo();
|
||||
Object.defineProperty(video, "readyState", {
|
||||
value: HTMLMediaElement.HAVE_METADATA,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(video, "videoWidth", { value: 0, configurable: true });
|
||||
Object.defineProperty(video, "videoHeight", { value: 0, configurable: true });
|
||||
document.body.appendChild(video);
|
||||
runtime = createColorGradingRuntime();
|
||||
const canvas = document.querySelector<HTMLCanvasElement>("[data-hf-color-grading-canvas]");
|
||||
if (!canvas) throw new Error("Expected color grading canvas");
|
||||
|
||||
const frame = document.createElement("img");
|
||||
frame.id = "__render_frame_hero-video__";
|
||||
frame.className = "__render_frame__";
|
||||
frame.style.border = "16px solid rgb(255, 0, 0)";
|
||||
Object.defineProperty(frame, "complete", { value: true, configurable: true });
|
||||
Object.defineProperty(frame, "naturalWidth", { value: 640, configurable: true });
|
||||
Object.defineProperty(frame, "naturalHeight", { value: 360, configurable: true });
|
||||
video.parentNode?.insertBefore(frame, canvas);
|
||||
video.style.setProperty("visibility", "hidden", "important");
|
||||
|
||||
runtime.redraw();
|
||||
|
||||
expect(canvas.style.display).toBe("block");
|
||||
expect(frame.style.borderStyle).toBe("none");
|
||||
|
||||
video.style.border = "16px dashed rgb(255, 0, 0)";
|
||||
video.removeAttribute(HF_COLOR_GRADING_ATTR);
|
||||
runtime.refresh();
|
||||
expect(frame.style.borderStyle).toBe("dashed");
|
||||
});
|
||||
|
||||
it("keeps a staged scene copy's canvas on its own render frame (#3994)", async () => {
|
||||
// Page-side shader transitions clone the scene, ids included, into a staging
|
||||
// layer. When both videos paired with the live frame, their canvases traded
|
||||
|
||||
@@ -210,6 +210,7 @@ interface ColorGradingEntry extends ColorGradingRenderer {
|
||||
touchedParent: HTMLElement | null;
|
||||
parentInlinePosition: string | null;
|
||||
sourceHidden: boolean;
|
||||
borderlessFrame: HTMLElement | null;
|
||||
sourceInlineOpacity: string | null;
|
||||
sourceInlineOpacityPriority: string;
|
||||
sourceOpacityForCanvas: string;
|
||||
@@ -1947,7 +1948,21 @@ function replaceProgramResources(entry: ColorGradingEntry): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideFrameBorder(entry: ColorGradingEntry, frame: HTMLElement): void {
|
||||
if (entry.borderlessFrame !== frame) restoreFrameBorder(entry);
|
||||
entry.borderlessFrame = frame;
|
||||
frame.style.borderStyle = "none";
|
||||
}
|
||||
|
||||
function restoreFrameBorder(entry: ColorGradingEntry): void {
|
||||
const frame = entry.borderlessFrame;
|
||||
if (!frame) return;
|
||||
entry.borderlessFrame = null;
|
||||
frame.style.borderStyle = window.getComputedStyle(entry.element).borderStyle;
|
||||
}
|
||||
|
||||
function restoreSourceElement(entry: ColorGradingEntry): void {
|
||||
restoreFrameBorder(entry);
|
||||
if (!entry.sourceHidden) return;
|
||||
entry.element.removeAttribute(COLOR_GRADING_SOURCE_HIDDEN_ATTR);
|
||||
const opacity = entry.element.style.getPropertyValue("opacity");
|
||||
@@ -2646,6 +2661,7 @@ function updateCanvasLayout(
|
||||
|
||||
const computed = window.getComputedStyle(styleSource);
|
||||
copyMediaVisualStyles(canvas.style, computed);
|
||||
canvas.style.borderStyle = "none";
|
||||
canvas.style.pointerEvents = "none";
|
||||
canvas.style.position = "absolute";
|
||||
canvas.style.inset = "auto";
|
||||
@@ -3182,6 +3198,7 @@ function drawEntry(entry: ColorGradingEntry): boolean {
|
||||
);
|
||||
drawFullscreenQuad(gl, program);
|
||||
hideSourceElement(entry);
|
||||
if (injectedFrameSource) hideFrameBorder(entry, source);
|
||||
entry.hasDrawn = true;
|
||||
entry.drawError = null;
|
||||
return true;
|
||||
@@ -3584,6 +3601,7 @@ export function createColorGradingRuntime(pausedMediaLease?: {
|
||||
touchedParent: null,
|
||||
parentInlinePosition: null,
|
||||
sourceHidden: false,
|
||||
borderlessFrame: null,
|
||||
sourceInlineOpacity: null,
|
||||
sourceInlineOpacityPriority: "",
|
||||
sourceOpacityForCanvas: window.getComputedStyle(element).opacity || "1",
|
||||
|
||||
@@ -1061,10 +1061,9 @@ async function initDrawElementOrTransparentBackground(
|
||||
const cssFx = await detectCssEffectRisk(page);
|
||||
if (cssFx) {
|
||||
session.deGateReason = `css_effect:${(cssFx.split(":")[0] ?? "").replace(/[^a-z-]/gi, "")}`;
|
||||
// Full specific effect ("filter:blur" / "filter:drop-shadow" /
|
||||
// "backdrop-filter" / "clip-path") — `deGateReason` sanitizes
|
||||
// this to the low-cardinality prefix; `deFallbackTrigger` keeps
|
||||
// the fine-grained value for the diagnostic profile emission.
|
||||
// Full specific effect from `detectCssEffectRisk` (e.g. "filter:blur",
|
||||
// "mix-blend-mode", "webgl-context") — `deGateReason` keeps its
|
||||
// low-cardinality prefix, `deFallbackTrigger` the full value.
|
||||
session.deFallbackTrigger = cssFx;
|
||||
console.log(
|
||||
`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
||||
|
||||
@@ -735,6 +735,15 @@ export async function injectVideoFramesBatch(
|
||||
? 1
|
||||
: opacityParsed;
|
||||
|
||||
// Measure first: an in-flow bordered <img> sibling would shrink the video's flex box.
|
||||
const videoRect = video.getBoundingClientRect();
|
||||
const videoBox = {
|
||||
left: Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0,
|
||||
top: Number.isFinite(video.offsetTop) ? video.offsetTop : 0,
|
||||
width: video.offsetWidth > 0 ? video.offsetWidth : videoRect.width,
|
||||
height: video.offsetHeight > 0 ? video.offsetHeight : videoRect.height,
|
||||
};
|
||||
|
||||
if (isNewImage) {
|
||||
img = document.createElement("img");
|
||||
img.classList.add("__render_frame__");
|
||||
@@ -770,24 +779,16 @@ export async function injectVideoFramesBatch(
|
||||
// instead of flowing below it. With position:relative, both elements
|
||||
// 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`;
|
||||
}
|
||||
img.style.position = "absolute";
|
||||
img.style.inset = "auto";
|
||||
img.style.left = `${videoBox.left}px`;
|
||||
img.style.top = `${videoBox.top}px`;
|
||||
img.style.right = "auto";
|
||||
img.style.bottom = "auto";
|
||||
img.style.width = `${videoBox.width}px`;
|
||||
img.style.height = `${videoBox.height}px`;
|
||||
// `videoBox` is a border-box even when the video is content-box.
|
||||
img.style.boxSizing = "border-box";
|
||||
img.style.objectFit = computedStyle.objectFit;
|
||||
img.style.objectPosition = computedStyle.objectPosition;
|
||||
img.style.zIndex = computedStyle.zIndex;
|
||||
|
||||
@@ -34,6 +34,7 @@ const INTEGRATION_TEST_FILES = new Set([
|
||||
"src/services/render/renderCancel.integration.test.ts",
|
||||
"src/services/render/stages/compileStage.mediaType.test.ts",
|
||||
"src/services/render/stages/gifEncodeArgs.test.ts",
|
||||
"src/services/videoFrameBorderClip.test.ts",
|
||||
"src/utils/audioRegression.test.ts",
|
||||
"src/utils/streamDurationParity.test.ts",
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import puppeteer, { type Browser, type Page } from "puppeteer";
|
||||
import { injectVideoFramesBatch } from "@hyperframes/engine";
|
||||
|
||||
/** Colour channels of one screenshot pixel. Alpha is always opaque here, so it is dropped. */
|
||||
type Rgb = { r: number; g: number; b: number };
|
||||
|
||||
/** A probe coordinate in screenshot space. */
|
||||
type Point = readonly [x: number, y: number];
|
||||
|
||||
const VIDEO_ID = "v1";
|
||||
|
||||
// Solid blue 1x1 PNG, stretched over the whole video box by the UA default
|
||||
// `object-fit: fill` — makes injected video content trivially distinguishable
|
||||
// from the white page background and the red border in a screenshot.
|
||||
const BLUE_PIXEL_DATA_URI =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYPj/HwADAgH/5ncLrgAAAABJRU5ErkJggg==";
|
||||
|
||||
const BOX_WIDTH = 200;
|
||||
const BOX_HEIGHT = 120;
|
||||
const BORDER_WIDTH = 8;
|
||||
const CORNER_RADIUS = 24;
|
||||
|
||||
/** Mid-way down the left border edge, far below the rounded corner's arc. */
|
||||
const BORDER_EDGE: Point = [BORDER_WIDTH / 2, BOX_HEIGHT / 2];
|
||||
/** Dead centre of the box, well inside the injected frame's content. */
|
||||
const VIDEO_CENTER: Point = [BOX_WIDTH / 2, BOX_HEIGHT / 2];
|
||||
/** Inside the corner that both the border-radius curve and the clip-path inset remove. */
|
||||
const CLIPPED_CORNER: Point = [1, 1];
|
||||
|
||||
async function readPixel(page: Page, screenshotBase64: string, [x, y]: Point): Promise<Rgb> {
|
||||
return page.evaluate(
|
||||
async (dataUri, probeX, probeY) => {
|
||||
const img = new Image();
|
||||
img.src = dataUri;
|
||||
await img.decode();
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("2d context unavailable");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const [r, g, b] = ctx.getImageData(probeX, probeY, 1, 1).data;
|
||||
if (r === undefined || g === undefined || b === undefined) {
|
||||
throw new Error(`no pixel data at (${probeX}, ${probeY})`);
|
||||
}
|
||||
return { r, g, b };
|
||||
},
|
||||
`data:image/png;base64,${screenshotBase64}`,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders the styled `<video>`, substitutes its frame, and returns a base64 PNG screenshot. */
|
||||
async function captureStyledVideoFrame(page: Page, videoAttributes: string): Promise<string> {
|
||||
await page.setContent(`<!doctype html>
|
||||
<style>html, body { margin: 0; padding: 0; background: #ffffff; }</style>
|
||||
<video
|
||||
id="${VIDEO_ID}"
|
||||
class="clip"
|
||||
muted
|
||||
${videoAttributes}
|
||||
style="position:absolute;left:0;top:0;width:${BOX_WIDTH}px;height:${BOX_HEIGHT}px;object-fit:fill;
|
||||
border:${BORDER_WIDTH}px solid red;border-radius:${CORNER_RADIUS}px;
|
||||
clip-path:inset(0 round ${CORNER_RADIUS}px);"
|
||||
></video>`);
|
||||
await injectVideoFramesBatch(page, [{ videoId: VIDEO_ID, dataUri: BLUE_PIXEL_DATA_URI }]);
|
||||
return page.screenshot({ type: "png", encoding: "base64" });
|
||||
}
|
||||
|
||||
function expectRedBorder(pixel: Rgb): void {
|
||||
expect(pixel.r).toBeGreaterThan(180);
|
||||
expect(pixel.b).toBeLessThan(80);
|
||||
}
|
||||
|
||||
function expectBlueVideoContent(pixel: Rgb): void {
|
||||
expect(pixel.b).toBeGreaterThan(180);
|
||||
}
|
||||
|
||||
function expectWhitePageBackground(pixel: Rgb): void {
|
||||
expect(pixel.r).toBeGreaterThan(240);
|
||||
expect(pixel.g).toBeGreaterThan(240);
|
||||
expect(pixel.b).toBeGreaterThan(240);
|
||||
}
|
||||
|
||||
describe("video border/border-radius/clip-path on the replacement render frame", () => {
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
beforeEach(async () => {
|
||||
page = await browser.newPage();
|
||||
await page.setViewport({ width: BOX_WIDTH, height: BOX_HEIGHT });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await page?.close();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close();
|
||||
});
|
||||
|
||||
// The border must reach the replacement <img>, not just the hidden <video>;
|
||||
// without border-width/style/color on the copy list the frame paints through it.
|
||||
it("paints a static border and clips to border-radius/clip-path on an untimed <video class=clip>", async () => {
|
||||
const screenshot = await captureStyledVideoFrame(page, "");
|
||||
|
||||
expectRedBorder(await readPixel(page, screenshot, BORDER_EDGE));
|
||||
expectBlueVideoContent(await readPixel(page, screenshot, VIDEO_CENTER));
|
||||
expectWhitePageBackground(await readPixel(page, screenshot, CLIPPED_CORNER));
|
||||
});
|
||||
|
||||
it("paints the same border/radius/clip-path on a timed <video data-start> clip", async () => {
|
||||
const screenshot = await captureStyledVideoFrame(page, 'data-start="0" data-duration="5"');
|
||||
|
||||
expectRedBorder(await readPixel(page, screenshot, BORDER_EDGE));
|
||||
expectBlueVideoContent(await readPixel(page, screenshot, VIDEO_CENTER));
|
||||
expectWhitePageBackground(await readPixel(page, screenshot, CLIPPED_CORNER));
|
||||
});
|
||||
|
||||
// A bordered <img> sibling still in flow shrinks a flex row's video, so the
|
||||
// video must be measured before the <img> is styled (the style-9-prod layout).
|
||||
it.each(["border-box", "content-box"])(
|
||||
"keeps the replacement <img>'s box identical to a %s video's own box in a flex-centered layout",
|
||||
async (boxSizing) => {
|
||||
const FLEX_ROW_WIDTH = 500;
|
||||
const FLEX_ROW_HEIGHT = 300;
|
||||
|
||||
await page.setViewport({ width: FLEX_ROW_WIDTH, height: FLEX_ROW_HEIGHT });
|
||||
await page.setContent(`<!doctype html>
|
||||
<style>
|
||||
* { box-sizing: ${boxSizing}; }
|
||||
html, body { margin: 0; padding: 0; background: #ffffff; }
|
||||
#aroll-container {
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
width: ${FLEX_ROW_WIDTH}px; height: ${FLEX_ROW_HEIGHT}px;
|
||||
}
|
||||
</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);
|
||||
const img = video?.nextElementSibling;
|
||||
if (!video || !img) throw new Error(`no replacement frame beside #${videoId}`);
|
||||
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);
|
||||
// Confirms the fixture gave the flex row room to shrink into.
|
||||
if (boxSizing === "border-box") expect(videoBox.width).toBe(FLEX_ROW_WIDTH);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8fb3bbb1666443c7f004a7b9c6dfba594097611fed18cf386e91fc45f5d378b
|
||||
size 13520567
|
||||
oid sha256:586aef50c55b7cc03d02b5c2f4200fd0a13453f1d09fbfe1ab221b0c71b88336
|
||||
size 13670413
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
],
|
||||
"sources": {
|
||||
"packages/producer/scripts/run-test-lane.mjs": "638885e14480d03290803389325a655f3749442167d784e07bb3af3404598af7",
|
||||
"packages/producer/scripts/test-classification.mjs": "d7ca46d663bfa7012147d1eff63a9e599f44393d7d65fd910d5b96cceb2b2f44",
|
||||
"packages/producer/scripts/test-classification.mjs": "5cf0848da08f0fd2c40521d1556cfdb00b5a072cbd0ea0bc54945990abb709e4",
|
||||
".github/workflows/ci.yml#job#producer-source-tests": "45da72aa6cf08cdfd5bcf5311e89f12af4842f9eebc6735df19da8c02f4d4864",
|
||||
"packages/producer/package.json#script#test:unit": "ae6beda8319d166774be958ea5593cd27dbf7f6c9bce1d787a3fe28dfc3477b1",
|
||||
"packages/producer/package.json#script#test:integration": "008589f37d026877595a13501cbb66abd5cbe50e21fdcac6904f45466c3f475d",
|
||||
|
||||
Reference in New Issue
Block a user