Bug 1956165 - Add a Fenix-only JS webcompat intervention for m.youtube.com to fix picture-in-picture; r=padenot,denschub,webcompat-reviewers,webidl,smaug
Differential Revision: https://phabricator.services.mozilla.com/D245108
This commit is contained in:
@@ -3370,5 +3370,22 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"1956165": {
|
||||
"label": "m.youtube.com picture-in-picture fix",
|
||||
"bugs": {
|
||||
"1956165": {
|
||||
"issue": "broken-videos",
|
||||
"matches": ["*://m.youtube.com/*"]
|
||||
}
|
||||
},
|
||||
"interventions": [
|
||||
{
|
||||
"platforms": ["fenix"],
|
||||
"content_scripts": {
|
||||
"js": ["bug1956165-www.youtube.com-picture-in-picture-fix.js"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ this.appConstants = class extends ExtensionAPI {
|
||||
getAPI() {
|
||||
return {
|
||||
appConstants: {
|
||||
getAndroidPackageName: () => {
|
||||
return Services.env.get("MOZ_ANDROID_PACKAGE_NAME");
|
||||
},
|
||||
getEffectiveUpdateChannel: () => {
|
||||
const ver = AppConstants.MOZ_APP_VERSION_DISPLAY;
|
||||
if (ver.includes("a")) {
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
"namespace": "appConstants",
|
||||
"description": "experimental API to expose some app constants",
|
||||
"functions": [
|
||||
{
|
||||
"name": "getAndroidPackageName",
|
||||
"type": "function",
|
||||
"description": "",
|
||||
"async": true,
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"name": "getEffectiveUpdateChannel",
|
||||
"type": "function",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Bug 1956165 - Fix picture-in-picture mode on Mobile YouTube
|
||||
*
|
||||
* YouTube does not play well with our picture in picture implementation, and
|
||||
* effectively cancels it. We can work around this conflict with this site patch.
|
||||
*/
|
||||
|
||||
/* globals exportFunction */
|
||||
|
||||
console.info(
|
||||
"exitFullscreen and pause have been overridden for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1956165 for details."
|
||||
);
|
||||
|
||||
const originalPause = window.wrappedJSObject.HTMLMediaElement.prototype.pause;
|
||||
|
||||
const newPause = exportFunction(function (...args) {
|
||||
if (this.ownerDocument.inAndroidPipMode) {
|
||||
return undefined;
|
||||
}
|
||||
return originalPause.apply(this, args);
|
||||
}, window);
|
||||
|
||||
Object.defineProperty(
|
||||
window.wrappedJSObject.HTMLMediaElement.prototype,
|
||||
"pause",
|
||||
{
|
||||
value: newPause,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
}
|
||||
);
|
||||
|
||||
const originalExitFullscreen =
|
||||
window.wrappedJSObject.Document.prototype.exitFullscreen;
|
||||
|
||||
const newExitFullscreen = exportFunction(function (...args) {
|
||||
if (!this.ownerDocument.inAndroidPipMode) {
|
||||
return undefined;
|
||||
}
|
||||
return originalExitFullscreen.apply(this, args);
|
||||
}, window);
|
||||
|
||||
Object.defineProperty(
|
||||
window.wrappedJSObject.Document.prototype,
|
||||
"exitFullscreen",
|
||||
{
|
||||
value: newExitFullscreen,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
}
|
||||
);
|
||||
@@ -280,7 +280,15 @@ var InterventionHelpers = {
|
||||
},
|
||||
},
|
||||
|
||||
valid_platforms: ["all", "android", "desktop", "linux", "mac", "windows"],
|
||||
valid_platforms: [
|
||||
"all",
|
||||
"android",
|
||||
"desktop",
|
||||
"fenix",
|
||||
"linux",
|
||||
"mac",
|
||||
"windows",
|
||||
],
|
||||
valid_channels: ["beta", "esr", "nightly", "stable"],
|
||||
|
||||
shouldSkip(intervention, firefoxVersion, firefoxChannel) {
|
||||
@@ -329,6 +337,12 @@ var InterventionHelpers = {
|
||||
platformInfo.os,
|
||||
platformInfo.os == "android" ? "android" : "desktop",
|
||||
];
|
||||
if (platformInfo.os == "android") {
|
||||
const packageName = await browser.appConstants.getAndroidPackageName();
|
||||
if (packageName.includes("fenix") || packageName.includes("firefox")) {
|
||||
InterventionHelpers._platformMatches.push("fenix");
|
||||
}
|
||||
}
|
||||
}
|
||||
return InterventionHelpers._platformMatches;
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "Web Compatibility Interventions",
|
||||
"description": "Urgent post-release fixes for web compatibility.",
|
||||
"version": "139.2.0",
|
||||
"version": "139.3.0",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "webcompat@mozilla.org",
|
||||
|
||||
@@ -1761,7 +1761,8 @@ bool Document::CallerIsTrustedAboutCertError(JSContext* aCx,
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Document::CallerCanAccessPrivilegeSSA(JSContext* aCx, JSObject* aObject) {
|
||||
bool Document::CallerIsSystemPrincipalOrWebCompatAddon(JSContext* aCx,
|
||||
JSObject* aObject) {
|
||||
RefPtr<BasePrincipal> principal =
|
||||
BasePrincipal::Cast(nsContentUtils::SubjectPrincipal(aCx));
|
||||
|
||||
@@ -1769,13 +1770,12 @@ bool Document::CallerCanAccessPrivilegeSSA(JSContext* aCx, JSObject* aObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We allow the privilege SSA to be called from system principal.
|
||||
// We allow the privileged APIs to be called from system principal.
|
||||
if (principal->IsSystemPrincipal()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We only allow calling the privilege SSA from the content script of the
|
||||
// webcompat extension.
|
||||
// We only allow calling privileged APIs from the webcompat extension.
|
||||
if (auto* policy = principal->ContentScriptAddonPolicy()) {
|
||||
nsAutoString addonID;
|
||||
policy->GetId(addonID);
|
||||
|
||||
@@ -2268,11 +2268,12 @@ class Document : public nsINode,
|
||||
static bool CallerIsTrustedAboutCertError(JSContext* aCx, JSObject* aObject);
|
||||
|
||||
/**
|
||||
* This function checks if the privilege storage access api is available for
|
||||
* the caller. We only allow privilege SSA to be called by system principal
|
||||
* and webcompat extension.
|
||||
* This function checks if the caller has access to privileged chrome APIs
|
||||
* such as the storage access API and inAndroidPipMode. We only allow such
|
||||
* APIs to be called by system principal and the built-in webcompat addon.
|
||||
*/
|
||||
static bool CallerCanAccessPrivilegeSSA(JSContext* aCx, JSObject* aObject);
|
||||
static bool CallerIsSystemPrincipalOrWebCompatAddon(JSContext* aCx,
|
||||
JSObject* aObject);
|
||||
|
||||
/**
|
||||
* Get the security info (i.e. certificate validity, errorCode, etc) for a
|
||||
|
||||
@@ -388,7 +388,7 @@ partial interface Document {
|
||||
// Whether we're in android's Picture-in-Picture mode.
|
||||
// Top level document only (for now, if we want to deal with iframes, please
|
||||
// also fix bug 1959448 while at it).
|
||||
[ChromeOnly]
|
||||
[Func="Document::CallerIsSystemPrincipalOrWebCompatAddon"]
|
||||
readonly attribute boolean inAndroidPipMode;
|
||||
|
||||
// The principal to use for the storage area of this document
|
||||
@@ -559,7 +559,7 @@ partial interface Document {
|
||||
// webcompat extension the ability to request the storage access for a given
|
||||
// third party.
|
||||
partial interface Document {
|
||||
[Func="Document::CallerCanAccessPrivilegeSSA", NewObject]
|
||||
[Func="Document::CallerIsSystemPrincipalOrWebCompatAddon", NewObject]
|
||||
Promise<undefined> requestStorageAccessForOrigin(DOMString thirdPartyOrigin, optional boolean requireUserInteraction = true);
|
||||
};
|
||||
|
||||
|
||||
@@ -328,7 +328,10 @@ async def session(driver, test_config):
|
||||
yield session
|
||||
|
||||
await session.bidi_session.end()
|
||||
session.end()
|
||||
try:
|
||||
session.end()
|
||||
except webdriver.error.UnknownErrorException:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -398,10 +401,11 @@ def only_firefox_versions(bug_number, firefox_version, request):
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def only_platforms(bug_number, platform, request, session):
|
||||
is_fenix = "org.mozilla.fenix" in session.capabilities.get("moz:profile", "")
|
||||
if request.node.get_closest_marker("only_platforms"):
|
||||
plats = request.node.get_closest_marker("only_platforms").args
|
||||
for only in plats:
|
||||
if only == platform:
|
||||
if only == platform or (only == "fenix" and is_fenix):
|
||||
return
|
||||
pytest.skip(
|
||||
f"Bug #{bug_number} skipped on platform ({platform}, test only for {' or '.join(plats)})"
|
||||
@@ -410,10 +414,11 @@ def only_platforms(bug_number, platform, request, session):
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def skip_platforms(bug_number, platform, request, session):
|
||||
is_fenix = "org.mozilla.fenix" in session.capabilities.get("moz:profile", "")
|
||||
if request.node.get_closest_marker("skip_platforms"):
|
||||
plats = request.node.get_closest_marker("skip_platforms").args
|
||||
for skipped in plats:
|
||||
if skipped == platform:
|
||||
if skipped == platform or (skipped == "fenix" and is_fenix):
|
||||
pytest.skip(
|
||||
f"Bug #{bug_number} skipped on platform ({platform}, test skipped for {' and '.join(plats)})"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
console_output_style = classic
|
||||
markers =
|
||||
only_firefox_versions: only run tests on specific firefox versions (specify min and/or max)
|
||||
only_platforms: only run tests on specific platforms (mac, linux, windows, android)
|
||||
only_platforms: only run tests on specific platforms (mac, linux, windows, android, fenix)
|
||||
skip_platforms: skip tests on specific platforms (mac, linux, windows, android)
|
||||
only_channels: only run tests on specific channels (beta, esr, nightly, stable)
|
||||
skip_channels: skip tests on specific channels (beta, esr, nightly, stable)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
URL = "https://m.youtube.com/results?search_query=trailer"
|
||||
FIRST_VIDEO_CSS = "a[href*=watch]"
|
||||
FULLSCREEN_ICON_CSS = "button.fullscreen-icon"
|
||||
PLAY_BUTTON_CSS = ".ytp-large-play-button.ytp-button"
|
||||
|
||||
|
||||
async def pip_activates_properly(client):
|
||||
await client.make_preload_script("delete navigator.__proto__.webdriver")
|
||||
|
||||
await client.navigate(URL)
|
||||
client.await_css(FIRST_VIDEO_CSS, is_displayed=True).click()
|
||||
|
||||
# wait for the video to start playing
|
||||
client.execute_async_script(
|
||||
"""
|
||||
const done = arguments[0];
|
||||
const i = setInterval(() => {
|
||||
const vid = document.querySelector("video");
|
||||
if (vid && !vid.paused) {
|
||||
done();
|
||||
}
|
||||
}, 100);
|
||||
"""
|
||||
)
|
||||
|
||||
client.soft_click(client.await_css(FULLSCREEN_ICON_CSS, is_displayed=True))
|
||||
|
||||
await asyncio.sleep(1)
|
||||
with client.using_context("chrome"):
|
||||
client.execute_script(
|
||||
"""
|
||||
ChromeUtils.androidMoveTaskToBack();
|
||||
"""
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
return client.execute_script(
|
||||
"""
|
||||
return !(document.querySelector("video")?.paused);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.only_platforms("fenix")
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.with_interventions
|
||||
async def test_enabled(client):
|
||||
assert await pip_activates_properly(client)
|
||||
|
||||
|
||||
@pytest.mark.only_platforms("fenix")
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.without_interventions
|
||||
async def test_disabled(client):
|
||||
assert not await pip_activates_properly(client)
|
||||
Reference in New Issue
Block a user