Files
Anarios e500088e65 Prepare 4.0.5 Firefox update and Shorts fixes
Add Firefox data consent, repair contributor sign-in, and introduce optional Hide clutter buttons. Harden video navigation, voting, and action layouts with shared browser regression coverage and reproducible release packaging.
2026-09-06 12:51:12 +02:00

221 lines
8.8 KiB
JavaScript

const assert = require("node:assert/strict");
function deepFreeze(value) {
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
Object.values(value).forEach(deepFreeze);
return Object.freeze(value);
}
const LIVE_RUNTIME_PROFILES = deepFreeze({
extension: {
capabilities: {
backgroundVoteTransport: true,
credentialStore: "browser.storage",
ownsShortsDislikeControl: true,
shortsControlModel: "synthetic-owned",
shortsControlModelBySurface: {
desktop: "synthetic-owned",
mobile: "native-youtube-required",
},
shortsVisualModel: "strict-synthetic",
},
buildMarkerAttribute: "data-ryd-extension-build",
markerAttribute: "data-ryd-extension-version",
runtime: "extension",
selectors: {
rateBar: "#ryd-bar",
rateBarContainer: "#ryd-bar-container",
shortsDislikeControl: "[data-ryd-synthetic-shorts-dislike]",
tooltipContent: "#ryd-dislike-tooltip",
tooltipTrigger: ".ryd-tooltip",
},
},
userscript: {
capabilities: {
backgroundVoteTransport: false,
credentialStore: "gm-storage",
ownsShortsDislikeControl: true,
shortsControlModel: "synthetic-owned",
shortsControlModelBySurface: {
desktop: "synthetic-owned",
mobile: "native-youtube-required",
},
shortsVisualModel: "strict-synthetic",
},
buildMarkerAttribute: "data-ryd-userscript-build",
markerAttribute: "data-ryd-userscript-version",
runtime: "userscript",
selectors: {
rateBar: "#return-youtube-dislike-bar",
rateBarContainer: "#return-youtube-dislike-bar-container",
shortsDislikeControl: "[data-ryd-synthetic-shorts-dislike]",
tooltipContent: ".ryd-tooltip-label",
tooltipTrigger: ".ryd-tooltip",
},
},
});
function requireDriverMethod(driver, method, scenarioId = null) {
if (typeof driver?.[method] === "function") return;
const scenario = scenarioId ? ` for shared scenario ${scenarioId}` : "";
throw new TypeError(`The ${driver ? "live driver" : "missing driver"}${scenario} must implement ${method}().`);
}
function assertRuntimeArgument(value, expected, label) {
if (value === undefined) return;
assert.equal(value, expected, `${label} does not match the selected live runtime adapter.`);
}
function createRuntimeBoundDriver(driver, runtime, expectedVersion, expectedBuildId, capabilities) {
return new Proxy(driver, {
get(target, property, receiver) {
if (property === "assertRuntime") {
return (requestedRuntime, requestedVersion, requestedBuildId) => {
assertRuntimeArgument(requestedRuntime, runtime, "The asserted runtime");
assertRuntimeArgument(requestedVersion, expectedVersion, "The asserted runtime version");
assertRuntimeArgument(requestedBuildId, expectedBuildId, "The asserted live build ID");
requireDriverMethod(target, "assertRuntime");
return target.assertRuntime(runtime, expectedVersion, expectedBuildId);
};
}
if (property === "assertCurrentShortsControl") {
return (videoId, requestedRuntime, assertionOptions) => {
assertRuntimeArgument(requestedRuntime, runtime, "The Shorts-control runtime");
requireDriverMethod(target, "assertCurrentShortsControl");
return assertionOptions === undefined
? target.assertCurrentShortsControl(videoId, runtime)
: target.assertCurrentShortsControl(videoId, runtime, assertionOptions);
};
}
if (property === "soakCurrentShortsControl") {
return (videoId, requestedRuntime, expectedDislikes, soakOptions) => {
assertRuntimeArgument(requestedRuntime, runtime, "The Shorts-control soak runtime");
requireDriverMethod(target, "soakCurrentShortsControl");
return target.soakCurrentShortsControl(videoId, runtime, expectedDislikes, soakOptions);
};
}
if (property === "assertCurrentWatchResult") {
return (videoId, requestedRuntime, expectedCounts, assertionOptions) => {
assertRuntimeArgument(requestedRuntime, runtime, "The Watch-result runtime");
requireDriverMethod(target, "assertCurrentWatchResult");
return assertionOptions === undefined
? target.assertCurrentWatchResult(videoId, runtime, expectedCounts)
: target.assertCurrentWatchResult(videoId, runtime, expectedCounts, assertionOptions);
};
}
if (property === "assertRenderedDislikeCount") {
return (renderedCount, dislikes, requestedRuntime) => {
assertRuntimeArgument(requestedRuntime, runtime, "The rendered-count runtime");
requireDriverMethod(target, "assertRenderedDislikeCount");
return target.assertRenderedDislikeCount(renderedCount, dislikes, runtime);
};
}
if (property === "assertDislikeCountChangesObservable") {
return (changes, requestedRuntime) => {
assertRuntimeArgument(requestedRuntime, runtime, "The reaction count-observability runtime");
requireDriverMethod(target, "assertDislikeCountChangesObservable");
return target.assertDislikeCountChangesObservable(changes, runtime);
};
}
if (property === "captureWatchRatioVisual") {
return (requestedRuntime, screenshotPath, options) => {
assertRuntimeArgument(requestedRuntime, runtime, "The watch-visual runtime");
requireDriverMethod(target, "captureWatchRatioVisual");
return options === undefined
? target.captureWatchRatioVisual(runtime, screenshotPath)
: target.captureWatchRatioVisual(runtime, screenshotPath, options);
};
}
if (property === "captureReactionStateVisual") {
return (request) => {
assertRuntimeArgument(request?.runtime, runtime, "The reaction-visual runtime");
assertRuntimeArgument(
request?.shortsVisualModel,
capabilities.shortsVisualModel,
"The reaction-visual Shorts model",
);
requireDriverMethod(target, "captureReactionStateVisual");
return target.captureReactionStateVisual({
...request,
runtime,
shortsVisualModel: capabilities.shortsVisualModel,
});
};
}
const value = Reflect.get(target, property, receiver);
return typeof value === "function" ? value.bind(target) : value;
},
});
}
class LiveRuntimeAdapter {
constructor({ driver, expectedBuildId, expectedVersion, runtime }) {
const profile = LIVE_RUNTIME_PROFILES[runtime];
if (!profile) throw new TypeError(`Unsupported live runtime adapter: ${runtime}`);
if (!driver || (typeof driver !== "object" && typeof driver !== "function")) {
throw new TypeError("A live YouTube driver is required.");
}
if (typeof expectedVersion !== "string" || expectedVersion.trim() === "") {
throw new TypeError("A non-empty expected runtime version is required.");
}
if (!/^[a-f0-9]{32}$/.test(expectedBuildId)) {
throw new TypeError("The expected live build ID must be a 32-character lowercase hexadecimal value.");
}
if (typeof driver.configureRequestAttributionRuntime === "function") {
driver.configureRequestAttributionRuntime(runtime);
}
this.capabilities = profile.capabilities;
this.driver = createRuntimeBoundDriver(driver, runtime, expectedVersion, expectedBuildId, this.capabilities);
this.expectedBuildId = expectedBuildId;
this.expectedVersion = expectedVersion;
this.profile = profile;
this.runtime = runtime;
this.selectors = profile.selectors;
}
createScenarioOptions(options = {}) {
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new TypeError("Shared live scenario options must be an object.");
}
assertRuntimeArgument(options.runtime, this.runtime, "The configured scenario runtime");
assertRuntimeArgument(options.expectedBuildId, this.expectedBuildId, "The configured live build ID");
assertRuntimeArgument(options.expectedVersion, this.expectedVersion, "The configured scenario runtime version");
return {
...options,
capabilities: this.capabilities,
expectedBuildId: this.expectedBuildId,
expectedVersion: this.expectedVersion,
runtime: this.runtime,
};
}
assertDriverMethods(methods, scenarioId) {
methods.forEach((method) => requireDriverMethod(this.driver, method, scenarioId));
}
}
function createExtensionLiveRuntimeAdapter(options) {
return new LiveRuntimeAdapter({ ...options, runtime: "extension" });
}
function createUserscriptLiveRuntimeAdapter(options) {
return new LiveRuntimeAdapter({ ...options, runtime: "userscript" });
}
module.exports = {
LIVE_RUNTIME_PROFILES,
LiveRuntimeAdapter,
createExtensionLiveRuntimeAdapter,
createUserscriptLiveRuntimeAdapter,
};