feat(controller): configure ambient video overlays

This commit is contained in:
joshpatra committed 2026-07-16 17:05:52 -04:00
1 parent 7073330916
commit 9c10183ab6
8 files changed
+236 -16

No files matched your search

+2
View File
@@ -25,6 +25,7 @@ describe("import/export flows", () => {
const settings = globalThis.vscGetSettingsDefaults();
settings.siteRules[1].title = "My Shorts rule";
settings.siteRules[1].shortcutTargetMode = "all";
settings.siteRules[1].showAmbientLoopControls = true;
const exported = globalThis.normalizedSettingsForExport(
globalThis.vscBuildStoredSettingsDiff(settings)
@@ -33,6 +34,7 @@ describe("import/export flows", () => {
expect(restored.siteRules[1].title).toBe("My Shorts rule");
expect(restored.siteRules[1].shortcutTargetMode).toBe("all");
expect(restored.siteRules[1].showAmbientLoopControls).toBe(true);
expect(exported.siteRules.map((rule) => rule.title)).toEqual([
"YouTube videos",
"My Shorts rule",
+70 -3
View File
@@ -309,7 +309,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
expect(second.video.playbackRate).toBe(1.2);
});
it("skips GIF-like ambient loops but accepts them if they become real players", async () => {
it("skips ambient loops by default and includes them when explicitly enabled", async () => {
bootInject();
await settleLifecycle();
@@ -332,12 +332,71 @@ describe("inject.js media/controller lifecycle regressions", () => {
expect(window.ensureController(video, mount)).toBeNull();
expect(video.vsc).toBeUndefined();
video.loop = false;
video.controls = true;
window.tc.settings.showAmbientLoopControls = true;
window.tc.siteRuleBase.showAmbientLoopControls = true;
expect(window.ensureController(video, mount)).toBeTruthy();
expect(video.vsc.div.isConnected).toBe(true);
});
it("always includes a genuine player even when its video has an ambient-loop signature", async () => {
bootInject();
await settleLifecycle();
const player = document.createElement("media-player");
const video = document.createElement("video");
const playButton = document.createElement("button");
const rect = makeRect(0, 0, 640, 360);
playButton.setAttribute("aria-label", "Play");
video.autoplay = true;
video.defaultMuted = true;
video.muted = true;
video.loop = true;
video.playsInline = true;
video.src = "https://example.org/interactive-loop.mp4";
player.append(video, playButton);
document.body.appendChild(player);
[player, video].forEach((element) => setRect(element, rect));
setBoxMetrics(player, rect.width, rect.height);
expect(window.tc.settings.showAmbientLoopControls).toBe(false);
expect(window.ensureController(video, player)).toBeTruthy();
expect(video.vsc.div.isConnected).toBe(true);
});
it("applies the ambient-loop setting from a matching site rule", async () => {
bootInject({
url: "https://news.example.org/story",
syncData: {
showAmbientLoopControls: false,
siteRules: [
{
pattern: "news.example.org",
enabled: true,
showAmbientLoopControls: true
}
]
}
});
await settleLifecycle();
const mount = document.createElement("div");
const video = document.createElement("video");
const rect = makeRect(0, 0, 480, 270);
video.autoplay = true;
video.defaultMuted = true;
video.muted = true;
video.loop = true;
video.playsInline = true;
video.src = "https://news.example.org/card.mp4";
mount.appendChild(video);
document.body.appendChild(mount);
[mount, video].forEach((element) => setRect(element, rect));
setBoxMetrics(mount, rect.width, rect.height);
expect(window.ensureController(video, mount)).toBeTruthy();
expect(window.tc.settings.showAmbientLoopControls).toBe(true);
});
it("moves the host into an ancestor fullscreen subtree and restores it", async () => {
bootInject();
await settleLifecycle();
@@ -610,6 +669,14 @@ describe("inject.js media/controller lifecycle regressions", () => {
controllerElement.dispatchEvent(new Event("pointerleave"));
expect(wrapper.classList.contains("ytp-autohide")).toBe(true);
player.dispatchEvent(new MouseEvent("mousemove", { bubbles: true }));
expect(wrapper.classList.contains("vsc-show")).toBe(true);
player.classList.remove("ytp-autohide");
player.classList.add("ytp-autohide");
await settleLifecycle();
expect(wrapper.classList.contains("ytp-autohide")).toBe(true);
expect(wrapper.classList.contains("vsc-show")).toBe(true);
});
it("suppresses a zero-size host and reveals it when the video becomes visible", async () => {
+10
View File
@@ -29,6 +29,7 @@ describe("options page", () => {
manifestVersion: "5.1.7.0",
sync: {
rememberSpeed: true,
showAmbientLoopControls: true,
enabled: false,
shortcutTargetMode: "all",
subtitleNudgeEnabledByDefault: false,
@@ -45,6 +46,7 @@ describe("options page", () => {
enabled: true,
shortcutTargetMode: "all",
preferredSpeed: 2.4,
showAmbientLoopControls: false,
subtitleNudgeEnabledByDefault: false,
showPopupControlBar: false,
popupControllerButtons: ["advance", "settings", "advance"]
@@ -55,6 +57,7 @@ describe("options page", () => {
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
expect(document.getElementById("rememberSpeed").checked).toBe(true);
expect(document.getElementById("showAmbientLoopControls").checked).toBe(true);
expect(document.getElementById("enabled").checked).toBe(false);
expect(document.getElementById("shortcutTargetMode").value).toBe("all");
expect(document.getElementById("subtitleNudgeEnabledByDefault").checked).toBe(
@@ -72,6 +75,9 @@ describe("options page", () => {
expect(document.querySelector(".site-rule .site-preferredSpeed").value).toBe(
"2.4"
);
expect(
document.querySelector(".site-rule .site-showAmbientLoopControls").checked
).toBe(false);
expect(
document.querySelector(".site-rule .site-shortcutTargetMode").value
).toBe("all");
@@ -181,6 +187,7 @@ describe("options page", () => {
const chrome = await setupOptions();
document.getElementById("rememberSpeed").checked = true;
document.getElementById("showAmbientLoopControls").checked = true;
document.getElementById("hideWithControlsTimer").value = "20";
document.getElementById("controllerOpacity").value = "0";
document.getElementById("controllerMarginTop").value = "250";
@@ -204,6 +211,7 @@ describe("options page", () => {
rule.querySelector(".site-pattern").value = "youtube.com";
rule.querySelector(".override-playback").checked = true;
rule.querySelector(".site-rememberSpeed").checked = true;
rule.querySelector(".site-showAmbientLoopControls").checked = false;
rule.querySelector(".site-preferredSpeed").value = "2.4";
rule.querySelector(".override-shortcut-target").checked = true;
rule.querySelector(".site-shortcutTargetMode").value = "all";
@@ -231,6 +239,7 @@ describe("options page", () => {
);
expect(savedSettings.rememberSpeed).toBe(true);
expect(savedSettings.showAmbientLoopControls).toBe(true);
expect(savedSettings.hideWithControlsTimer).toBe(15);
expect(savedSettings.controllerOpacity).toBe(0);
expect(savedSettings.controllerMarginTop).toBe(200);
@@ -246,6 +255,7 @@ describe("options page", () => {
title: "My YouTube rule",
pattern: "youtube.com",
rememberSpeed: true,
showAmbientLoopControls: false,
shortcutTargetMode: "all",
preferredSpeed: 2.4,
controllerOpacity: 0,
+26
View File
@@ -34,10 +34,36 @@ describe("canonical settings storage", () => {
})
);
expect(settings.shortcutTargetMode).toBe("closest");
expect(settings.showAmbientLoopControls).toBe(false);
expect(settings.subtitleNudgeInterval).toBe(250);
expect(settings.siteRules[0].enableSubtitleNudge).toBeUndefined();
});
it("round-trips global and site-specific ambient-loop visibility", () => {
const settings = window.vscExpandStoredSettings({
showAmbientLoopControls: true,
siteRules: [
{
pattern: "cnn.com",
showAmbientLoopControls: false
}
]
});
const restored = window.vscExpandStoredSettings(
window.vscBuildStoredSettingsDiff(settings)
);
expect(restored.showAmbientLoopControls).toBe(true);
expect(restored.siteRules).toEqual(
expect.arrayContaining([
expect.objectContaining({
pattern: "cnn.com",
showAmbientLoopControls: false
})
])
);
});
it("round-trips global and site-specific shortcut targeting", () => {
const settings = window.vscExpandStoredSettings({
shortcutTargetMode: "all",