mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +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", () => {
|
||||
const find = (findings: Array<{ code: string }>) =>
|
||||
findings.find((f) => f.code === "subcomposition_blanks_before_host");
|
||||
|
||||
@@ -634,6 +634,38 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
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
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user