fix(lint): detect optional managed media calls (#5210)

This commit is contained in:
Abdullah M. Nazif
2026-10-08 20:23:08 +00:00
committed by GitHub
parent c94d992188
commit c03aa9224d
2 changed files with 72 additions and 6 deletions
+49
View File
@@ -449,6 +449,55 @@ describe("media rules", () => {
expect(finding?.elementId).toBe("demo-video");
});
function optionalMediaScene(script: string): string {
return `<html><body>
<div id="root" data-composition-id="main" data-width="640" data-height="360">
<video id="demo-video" class="demo-video" src="clip.mp4" muted data-start="0" data-duration="1"></video>
<div id="panel"></div>
</div>
<script>${script}</script>
</body></html>`;
}
it.each([
'const video = document.getElementById("demo-video"); video?.play();',
'const video = document.getElementById("demo-video"); video.play?.();',
'const video = document.getElementById("demo-video"); video?.play?.();',
'const video = document.querySelector("#demo-video"); video?.pause();',
'const video = document.querySelector(".demo-video"); video.pause?.();',
'document.getElementById("demo-video")?.play();',
'document.getElementById("demo-video").pause?.();',
'document.querySelector("#demo-video")?.play?.();',
'window.document.querySelector("video")?.pause();',
'const video = document.getElementById("demo-video"); const label = `playing ${video?.play()}`;',
])("reports an executed optional media call: %s", async (script) => {
const result = await lintHyperframeHtml(optionalMediaScene(script));
expect(
result.findings.find((finding) => finding.code === "imperative_media_control"),
).toMatchObject({
severity: "error",
});
});
it.each([
'const video = document.getElementById("demo-video"); const example = "video.play()";',
'const video = document.getElementById("demo-video"); // video.play()\n',
'const video = document.getElementById("demo-video"); const example = `video.play()`;',
'const video = document.getElementById("demo-video"); const example = /video.play()/;',
'const video = document.getElementById("demo-video"); const example = "video?.play?.()";',
'const video = document.getElementById("demo-video"); /* video?.play() */',
'const video = document.getElementById("demo-video"); const example = `video.pause?.()`;',
`const example = 'document.getElementById("demo-video").play()';`,
`const example = 'document.querySelector("video")?.play()';`,
'const panel = document.getElementById("panel"); panel?.play?.();',
'document.querySelector("#panel")?.pause();',
])("does not report a quoted example or non-media optional call: %s", async (script) => {
const result = await lintHyperframeHtml(optionalMediaScene(script));
expect(result.findings.map((finding) => finding.code)).not.toContain(
"imperative_media_control",
);
});
it("reports imperative currentTime writes on query-selected managed media", async () => {
const html = `
<html><body>
+23 -6
View File
@@ -3,6 +3,7 @@ import {
readAttr,
readDecodedAttr,
stripJsComments,
stripJsCode,
truncateSnippet,
isMediaTag,
hasAttrName,
@@ -230,6 +231,7 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
if (mediaTags.length === 0 || ctx.scripts.length === 0) return findings;
for (const script of ctx.scripts) {
const code = stripJsCode(script.content);
const mediaVars = new Map<string, string | undefined>();
const assignmentPatterns = [
{
@@ -249,6 +251,7 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
for (const { pattern, variableIndex, targetIndex } of assignmentPatterns) {
let match: RegExpExecArray | null;
while ((match = pattern.exec(script.content)) !== null) {
if (code[match.index] !== script.content[match.index]) continue;
const variableName = match[variableIndex];
const target = match[targetIndex];
if (!variableName || !target) continue;
@@ -261,13 +264,13 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
const directIdPatterns = [
{
pattern:
/\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\.play\s*\(/g,
/\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\s*(?:\?\.|\.)\s*play\s*(?:\?\.\s*)?\(/g,
kind: "play()",
targetIndex: 1,
},
{
pattern:
/\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\.pause\s*\(/g,
/\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\s*(?:\?\.|\.)\s*pause\s*(?:\?\.\s*)?\(/g,
kind: "pause()",
targetIndex: 1,
},
@@ -285,13 +288,13 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
},
{
pattern:
/\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\.play\s*\(/g,
/\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\s*(?:\?\.|\.)\s*play\s*(?:\?\.\s*)?\(/g,
kind: "play()",
targetIndex: 2,
},
{
pattern:
/\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\.pause\s*\(/g,
/\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\s*(?:\?\.|\.)\s*pause\s*(?:\?\.\s*)?\(/g,
kind: "pause()",
targetIndex: 2,
},
@@ -312,6 +315,7 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
for (const { pattern, kind, targetIndex } of directIdPatterns) {
let match: RegExpExecArray | null;
while ((match = pattern.exec(script.content)) !== null) {
if (code[match.index] !== script.content[match.index]) continue;
const target = match[targetIndex];
if (!target) continue;
const elementId = mediaIndex.ids.has(target)
@@ -335,8 +339,20 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
for (const [variableName, elementId] of mediaVars) {
const escapedVar = escapeRegExp(variableName);
const variablePatterns = [
{ pattern: new RegExp(`\\b${escapedVar}\\.play\\s*\\(`, "g"), kind: "play()" },
{ pattern: new RegExp(`\\b${escapedVar}\\.pause\\s*\\(`, "g"), kind: "pause()" },
{
pattern: new RegExp(
`\\b${escapedVar}\\s*(?:\\?\\.|\\.)\\s*play\\s*(?:\\?\\.\\s*)?\\(`,
"g",
),
kind: "play()",
},
{
pattern: new RegExp(
`\\b${escapedVar}\\s*(?:\\?\\.|\\.)\\s*pause\\s*(?:\\?\\.\\s*)?\\(`,
"g",
),
kind: "pause()",
},
{ pattern: new RegExp(`\\b${escapedVar}\\.currentTime\\s*=`, "g"), kind: "currentTime" },
{
pattern: new RegExp(`\\b${escapedVar}\\.muted\\s*=`, "g"),
@@ -346,6 +362,7 @@ function findImperativeMediaControlFindings(ctx: LintContext): HyperframeLintFin
for (const { pattern, kind } of variablePatterns) {
let match: RegExpExecArray | null;
while ((match = pattern.exec(script.content)) !== null) {
if (code[match.index] !== script.content[match.index]) continue;
findings.push({
code: "imperative_media_control",
severity: "error",