mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): honor HTML attribute casing in frame-rate defaults (#5348)
* fix(cli): honor HTML attribute casing in frame-rate defaults * chore: prepare conflict resolution against current main * fix(cli): preserve HTML fps casing with shared cloud defaults
This commit is contained in:
@@ -12,6 +12,43 @@ describe("readCompositionFps", () => {
|
||||
expect(readCompositionFps(html)).toBe("24");
|
||||
});
|
||||
|
||||
it.each(["DATA-FPS", "Data-FpS"])("reads an HTML %s attribute", (attribute) => {
|
||||
const html = wrap(`<div data-composition-id="root" data-root="true" ${attribute}="24">x</div>`);
|
||||
expect(readCompositionFps(html)).toBe("24");
|
||||
});
|
||||
|
||||
it("prefers an explicit root with mixed-case HTML attribute names", () => {
|
||||
const html = wrap(
|
||||
'<div data-composition-id="other" data-root="TRUE" data-fps="60"></div>' +
|
||||
'<div DATA-COMPOSITION-ID="root" Data-Root="true" DATA-FPS="24">x</div>',
|
||||
);
|
||||
expect(readCompositionFps(html)).toBe("24");
|
||||
});
|
||||
|
||||
it("reads the outermost composition with mixed-case HTML attribute names", () => {
|
||||
const html = wrap(
|
||||
'<div DATA-COMPOSITION-ID="root" Data-Fps="48"><div data-composition-id="child" data-fps="12">x</div></div>',
|
||||
);
|
||||
expect(readCompositionFps(html)).toBe("48");
|
||||
});
|
||||
|
||||
it.each([
|
||||
'DATA-FPS="24" data-fps="60"',
|
||||
'Data-FpS="24" DATA-FPS="60"',
|
||||
'data-fps="24" DATA-FPS="60"',
|
||||
])("keeps the first duplicate HTML attribute in %s", (attributes) => {
|
||||
const html = wrap(`<div data-composition-id="root" ${attributes}>x</div>`);
|
||||
expect(readCompositionFps(html)).toBe("24");
|
||||
});
|
||||
|
||||
it("keeps the first duplicate data-root value when selecting the root", () => {
|
||||
const html = wrap(
|
||||
'<div data-composition-id="other" DATA-ROOT="false" data-root="true" data-fps="60"></div>' +
|
||||
'<div data-composition-id="root" data-root="true" data-fps="24">x</div>',
|
||||
);
|
||||
expect(readCompositionFps(html)).toBe("24");
|
||||
});
|
||||
|
||||
it("reads data-fps from the outermost composition when no data-root is marked", () => {
|
||||
const html = wrap(
|
||||
'<div data-composition-id="root" data-fps="48"><div data-composition-id="child" data-fps="12">x</div></div>',
|
||||
@@ -77,6 +114,16 @@ describe("defaultCloudFps", () => {
|
||||
expect(String(warn.mock.calls[0]?.[0])).toContain(`data-fps="${fps}"`);
|
||||
});
|
||||
|
||||
it("reads mixed-case HTML attributes before validating the cloud fps", () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const dir = makeProject(
|
||||
'<div DATA-COMPOSITION-ID="root" DATA-ROOT="true" DATA-FPS="24">x</div>',
|
||||
);
|
||||
|
||||
expect(defaultCloudFps(dir)).toBe(24);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses 30 without warning when index.html has no data-fps", () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
expect(
|
||||
|
||||
@@ -4,6 +4,14 @@ import { DISTRIBUTED_FPS, type DistributedFps, isDistributedFps } from "@hyperfr
|
||||
import { parseHTML } from "linkedom";
|
||||
import { c } from "../ui/colors.js";
|
||||
|
||||
function getHtmlAttribute(element: Element, name: string): string | null {
|
||||
// linkedom preserves spelling; HTML names match without case and keep the first duplicate.
|
||||
return (
|
||||
Array.from(element.attributes).find((attribute) => attribute.name.toLowerCase() === name)
|
||||
?.value ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a composition's declared frame rate from its root element's `data-fps`
|
||||
* attribute — the same attribute the runtime honors (core/runtime/init.ts) — so
|
||||
@@ -24,15 +32,14 @@ export function readCompositionFps(html: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const explicitRoot = doc.querySelector('[data-composition-id][data-root="true"]');
|
||||
const compositions = Array.from(doc.querySelectorAll("*")).filter(
|
||||
(element) => getHtmlAttribute(element, "data-composition-id") !== null,
|
||||
);
|
||||
const root =
|
||||
explicitRoot ??
|
||||
Array.from(doc.querySelectorAll("[data-composition-id]")).find(
|
||||
(el) => !el.parentElement?.closest("[data-composition-id]"),
|
||||
) ??
|
||||
null;
|
||||
compositions.find((element) => getHtmlAttribute(element, "data-root") === "true") ??
|
||||
compositions[0];
|
||||
|
||||
const raw = root?.getAttribute("data-fps")?.trim();
|
||||
const raw = root ? getHtmlAttribute(root, "data-fps")?.trim() : undefined;
|
||||
return raw ? raw : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +332,21 @@ describe("resolveDefaultFpsArg", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses an uppercase HTML data-fps declaration when --fps is omitted", () => {
|
||||
const project = makeProject();
|
||||
try {
|
||||
writeFileSync(
|
||||
project.indexPath,
|
||||
'<!DOCTYPE html><html><body><div data-composition-id="root" DATA-FPS="24">x</div></body></html>',
|
||||
);
|
||||
|
||||
expect(resolveDefaultFpsArg(undefined, project.dir, project.indexPath, undefined)).toBe("24");
|
||||
expect(resolveDefaultFpsArg("60", project.dir, project.indexPath, undefined)).toBe("60");
|
||||
} finally {
|
||||
project.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an explicit --fps value ahead of any composition default", () => {
|
||||
const project = makeProject();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user