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:
Abdullah M. Nazif
2026-10-24 04:10:49 +00:00
committed by GitHub
parent 234b87a4bb
commit c00a1bc1d4
3 changed files with 76 additions and 7 deletions
@@ -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(
+14 -7
View File
@@ -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;
}
+15
View File
@@ -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 {