mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(lint): warn when <html dir> can blank render output (#1893)
* feat(lint): flag dir="rtl" on <html> as a confirmed silent render failure Two independent reports diagnosed the same exact bug: dir="rtl" (or any non-ltr value) on <html> renders correctly in preview/snapshot but produces a fully blank/black video from render, with no other lint/validate/inspect check catching it - output file size (far smaller than expected) was the only tell for both reporters. Both independently confirmed the same fix: drop dir from <html>, keep lang, and scope direction: rtl to individual text-containing elements via CSS instead. Could not empirically verify the render pipeline's own root cause in this session (headless Chrome screenshot capture is unreliable in this sandboxed environment - even a baseline, non-RTL capture timed out), so this ships the safe, already-confirmed advisory rather than guessing at a runtime fix. Both reporters explicitly asked for exactly this: "deserves a lint rule or render-time warning." * fix(lint): only flag valid non-ltr html dir values
This commit is contained in:
@@ -1009,6 +1009,70 @@ describe("composition rules", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("html_dir_attribute_breaks_render", () => {
|
||||||
|
const CODE = "html_dir_attribute_breaks_render";
|
||||||
|
const find = (findings: { code: string }[]) => findings.find((f) => f.code === CODE);
|
||||||
|
|
||||||
|
it('flags dir="rtl" on <html>', async () => {
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<body>
|
||||||
|
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="5">مرحبا</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
const result = await lintHyperframeHtml(html);
|
||||||
|
const finding = find(result.findings);
|
||||||
|
expect(finding).toBeDefined();
|
||||||
|
expect(finding?.severity).toBe("error");
|
||||||
|
expect(finding?.message).toContain('dir="rtl"');
|
||||||
|
expect(finding?.fixHint).toContain("direction: rtl");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags dir="auto" on <html>', async () => {
|
||||||
|
const html = `<html dir="AUTO"><body>
|
||||||
|
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="5"></div>
|
||||||
|
</body></html>`;
|
||||||
|
const result = await lintHyperframeHtml(html);
|
||||||
|
const finding = find(result.findings);
|
||||||
|
expect(finding).toBeDefined();
|
||||||
|
expect(finding?.fixHint).toContain('dir="auto"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not flag dir="ltr"', async () => {
|
||||||
|
const html = `<html dir="ltr"><body>
|
||||||
|
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="5"></div>
|
||||||
|
</body></html>`;
|
||||||
|
const result = await lintHyperframeHtml(html);
|
||||||
|
expect(find(result.findings)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not flag invalid dir values that browsers treat as ltr", async () => {
|
||||||
|
const html = `<html dir="bogus"><body>
|
||||||
|
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="5"></div>
|
||||||
|
</body></html>`;
|
||||||
|
const result = await lintHyperframeHtml(html);
|
||||||
|
expect(find(result.findings)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not flag when <html> has no dir attribute", async () => {
|
||||||
|
const html = `<html><body>
|
||||||
|
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="5"></div>
|
||||||
|
</body></html>`;
|
||||||
|
const result = await lintHyperframeHtml(html);
|
||||||
|
expect(find(result.findings)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not flag dir="rtl" scoped to an individual element (the documented fix)', async () => {
|
||||||
|
const html = `<html><body>
|
||||||
|
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="5">
|
||||||
|
<p style="direction: rtl;">مرحبا</p>
|
||||||
|
</div>
|
||||||
|
</body></html>`;
|
||||||
|
const result = await lintHyperframeHtml(html);
|
||||||
|
expect(find(result.findings)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("subcomposition_blanks_before_host", () => {
|
describe("subcomposition_blanks_before_host", () => {
|
||||||
const find = (findings: Array<{ code: string }>) =>
|
const find = (findings: Array<{ code: string }>) =>
|
||||||
findings.find((f) => f.code === "subcomposition_blanks_before_host");
|
findings.find((f) => f.code === "subcomposition_blanks_before_host");
|
||||||
|
|||||||
@@ -634,6 +634,38 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
|||||||
return findings;
|
return findings;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// html_dir_attribute_breaks_render — valid non-LTR dir values on
|
||||||
|
// <html> renders correctly in preview/snapshot but produces a fully
|
||||||
|
// blank/black video from render, with no other lint/validate/inspect
|
||||||
|
// check catching it (output file size, far smaller than expected, is the
|
||||||
|
// only tell). Confirmed independently by two separate reports, both
|
||||||
|
// diagnosing the same exact trigger and the same fix: drop dir from
|
||||||
|
// <html>, keep lang, and scope `direction: rtl` to individual
|
||||||
|
// text-containing elements via CSS instead (text still bidi-shapes
|
||||||
|
// correctly). Advisory-only — this does not attempt to fix the render
|
||||||
|
// pipeline's own root cause (suspected to be a capture step that clips a
|
||||||
|
// fixed top-left-origin screenshot region, which RTL layout can shift the
|
||||||
|
// actual content away from), only surfaces the already-confirmed footgun
|
||||||
|
// before someone hits it blind.
|
||||||
|
({ source }) => {
|
||||||
|
const htmlTag = findHtmlTag(source);
|
||||||
|
if (!htmlTag) return [];
|
||||||
|
const dir = readAttr(htmlTag.raw, "dir");
|
||||||
|
if (!dir) return [];
|
||||||
|
const normalizedDir = dir.toLowerCase();
|
||||||
|
if (normalizedDir !== "rtl" && normalizedDir !== "auto") return [];
|
||||||
|
const scopedDirection = normalizedDir === "auto" ? 'dir="auto"' : `direction: ${normalizedDir}`;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
code: "html_dir_attribute_breaks_render",
|
||||||
|
severity: "error",
|
||||||
|
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render — a confirmed, silent failure.`,
|
||||||
|
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead — text still shapes correctly via the browser's own bidi algorithm.`,
|
||||||
|
snippet: truncateSnippet(htmlTag.raw),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
// subcomposition_blanks_before_host
|
// subcomposition_blanks_before_host
|
||||||
// Warns when a full-bleed sub-composition slot ends before the host composition
|
// Warns when a full-bleed sub-composition slot ends before the host composition
|
||||||
// does, leaving the slot blank for the remainder (issue #1540). Scoped narrowly to
|
// does, leaving the slot blank for the remainder (issue #1540). Scoped narrowly to
|
||||||
|
|||||||
Reference in New Issue
Block a user