Files
return-youtube-dislike/Extensions/combined/src/state.js
T
Leon Bubova d62130b898 Feature/add unit testing framework and start writing unit tests (acceptance tests) (#708)
* add jest and rewire for testing

* wrap loose code for initMutationObserver() in function and call it instead of just executing it

Otherwise this code is executed everytime state.js is imported somewhere in a file.
This leads to not being able to test properly, since state.js is imported in a lot
of the src files and fails when running the tests since we are missing global environment
settings such as document, window etc in the jest environment.

* export roundDown and getNumberformatter to make them testable as well

* add basic acceptance tests for utils.js, create snapshots and add jest config

* add failing test for buggy behaviour in a separate test case and skip it until fixed

* Fixes after merge conflicts

---------

Co-authored-by: Dmitrii Selivanov <selivano.d@gmail.com>
2024-06-16 19:52:58 +02:00

402 lines
12 KiB
JavaScript

import { getLikeButton, getDislikeButton, getButtons, getLikeTextContainer, getDislikeTextContainer } from "./buttons";
import { createRateBar } from "./bar";
import {
getBrowser,
getVideoId,
cLog,
numberFormat,
getColorFromTheme,
querySelector,
localize,
createObserver,
} from "./utils";
//TODO: Do not duplicate here and in ryd.background.js
const apiUrl = "https://returnyoutubedislikeapi.com";
const LIKED_STATE = "LIKED_STATE";
const DISLIKED_STATE = "DISLIKED_STATE";
const NEUTRAL_STATE = "NEUTRAL_STATE";
let extConfig = {
disableVoteSubmission: false,
disableLogging: false,
coloredThumbs: false,
coloredBar: false,
colorTheme: "classic",
numberDisplayFormat: "compactShort",
showTooltipPercentage: false,
tooltipPercentageMode: "dash_like",
numberDisplayReformatLikes: false,
selectors: {
dislikeTextContainer: [],
likeTextContainer: [],
buttons: {
shorts: {
mobile: [],
desktop: [],
},
regular: {
mobile: [],
desktopMenu: [],
desktopNoMenu: [],
},
likeButton: {
segmented: [],
segmentedGetButtons: [],
notSegmented: [],
},
dislikeButton: {
segmented: [],
segmentedGetButtons: [],
notSegmented: [],
},
},
menuContainer: [],
roundedDesign: [],
},
};
let storedData = {
likes: 0,
dislikes: 0,
previousState: NEUTRAL_STATE,
};
function isMobile() {
return location.hostname == "m.youtube.com";
}
function isShorts() {
return location.pathname.startsWith("/shorts");
}
function isNewDesign() {
return document.getElementById("comment-teaser") !== null;
}
function isRoundedDesign() {
return querySelector(extConfig.selectors.roundedDesign) !== null;
}
let shortsObserver = null;
if (isShorts() && !shortsObserver) {
cLog("Initializing shorts mutation observer");
shortsObserver = createObserver(
{
attributes: true,
},
(mutationList) => {
mutationList.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.target.nodeName === "TP-YT-PAPER-BUTTON" &&
mutation.target.id === "button"
) {
// cLog('Short thumb button status changed');
if (mutation.target.getAttribute("aria-pressed") === "true") {
mutation.target.style.color =
mutation.target.parentElement.parentElement.id === "like-button"
? getColorFromTheme(true)
: getColorFromTheme(false);
} else {
mutation.target.style.color = "unset";
}
return;
}
cLog("Unexpected mutation observer event: " + mutation.target + mutation.type);
});
},
);
}
function isLikesDisabled() {
// return true if the like button's text doesn't contain any number
if (isMobile()) {
return /^\D*$/.test(getButtons().children[0].querySelector(".button-renderer-text").innerText);
}
return /^\D*$/.test(getLikeTextContainer().innerText);
}
function isVideoLiked() {
if (isMobile()) {
return getLikeButton().querySelector("button").getAttribute("aria-label") === "true";
}
return (
getLikeButton().classList.contains("style-default-active") ||
getLikeButton().querySelector("button")?.getAttribute("aria-pressed") === "true"
);
}
function isVideoDisliked() {
if (isMobile()) {
return getDislikeButton().querySelector("button").getAttribute("aria-label") === "true";
}
return (
getDislikeButton().classList.contains("style-default-active") ||
getDislikeButton().querySelector("button")?.getAttribute("aria-pressed") === "true"
);
}
function getState(storedData) {
if (isVideoLiked()) {
return { current: LIKED_STATE, previous: storedData.previousState };
}
if (isVideoDisliked()) {
return { current: DISLIKED_STATE, previous: storedData.previousState };
}
return { current: NEUTRAL_STATE, previous: storedData.previousState };
}
//--- Sets The Likes And Dislikes Values ---//
function setLikes(likesCount) {
cLog(`SET likes ${likesCount}`);
getLikeTextContainer().innerText = likesCount;
}
function setDislikes(dislikesCount) {
cLog(`SET dislikes ${dislikesCount}`);
getDislikeTextContainer()?.removeAttribute("is-empty");
if (!isLikesDisabled()) {
if (isMobile()) {
getButtons().children[1].querySelector(".button-renderer-text").innerText = dislikesCount;
return;
}
getDislikeTextContainer().innerText = dislikesCount;
} else {
cLog("likes count disabled by creator");
if (isMobile()) {
getButtons().children[1].querySelector(".button-renderer-text").innerText = localize("TextLikesDisabled");
return;
}
getDislikeTextContainer().innerText = localize("TextLikesDisabled");
}
}
function getLikeCountFromButton() {
try {
if (isShorts()) {
//Youtube Shorts don't work with this query. It's not necessary; we can skip it and still see the results.
//It should be possible to fix this function, but it's not critical to showing the dislike count.
return false;
}
let likeButton =
getLikeButton().querySelector("yt-formatted-string#text") ?? getLikeButton().querySelector("button");
let likesStr = likeButton.getAttribute("aria-label").replace(/\D/g, "");
return likesStr.length > 0 ? parseInt(likesStr) : false;
} catch {
return false;
}
}
function processResponse(response, storedData) {
const formattedDislike = numberFormat(response.dislikes);
setDislikes(formattedDislike);
if (extConfig.numberDisplayReformatLikes === true) {
const nativeLikes = getLikeCountFromButton();
if (nativeLikes !== false) {
setLikes(numberFormat(nativeLikes));
}
}
storedData.dislikes = parseInt(response.dislikes);
storedData.likes = getLikeCountFromButton() || parseInt(response.likes);
createRateBar(storedData.likes, storedData.dislikes);
if (extConfig.coloredThumbs === true) {
if (isShorts()) {
// for shorts, leave deactivated buttons in default color
let shortLikeButton = getLikeButton().querySelector("tp-yt-paper-button#button");
let shortDislikeButton = getDislikeButton().querySelector("tp-yt-paper-button#button");
if (shortLikeButton.getAttribute("aria-pressed") === "true") {
shortLikeButton.style.color = getColorFromTheme(true);
}
if (shortDislikeButton.getAttribute("aria-pressed") === "true") {
shortDislikeButton.style.color = getColorFromTheme(false);
}
shortsObserver.observe(shortLikeButton);
shortsObserver.observe(shortDislikeButton);
} else {
getLikeButton().style.color = getColorFromTheme(true);
getDislikeButton().style.color = getColorFromTheme(false);
}
}
//Temporary disabling this - it breaks all places where getButtons()[1] is used
// createStarRating(response.rating, isMobile());
}
// Tells the user if the API is down
function displayError(error) {
getDislikeTextContainer().innerText = localize("textTempUnavailable");
}
async function setState(storedData) {
storedData.previousState = isVideoDisliked() ? DISLIKED_STATE : isVideoLiked() ? LIKED_STATE : NEUTRAL_STATE;
let statsSet = false;
cLog("Video is loaded. Adding buttons...");
let videoId = getVideoId(window.location.href);
let likeCount = getLikeCountFromButton() || null;
let response = await fetch(`${apiUrl}/votes?videoId=${videoId}&likeCount=${likeCount || ""}`, {
method: "GET",
headers: {
Accept: "application/json",
},
})
.then((response) => {
if (!response.ok) displayError(response.error);
return response;
})
.then((response) => response.json())
.catch(displayError);
cLog("response from api:");
cLog(JSON.stringify(response));
if (response !== undefined && !("traceId" in response) && !statsSet) {
processResponse(response, storedData);
}
}
async function setInitialState() {
await setState(storedData);
}
async function initExtConfig() {
initializeDisableVoteSubmission();
initializeDisableLogging();
initializeColoredThumbs();
initializeColoredBar();
initializeColorTheme();
initializeNumberDisplayFormat();
initializeTooltipPercentage();
initializeTooltipPercentageMode();
initializeNumberDisplayReformatLikes();
await initializeSelectors();
}
async function initializeSelectors() {
console.log("initializing selectors");
let result = await fetch(`${apiUrl}/configs/selectors`, {
method: "GET",
headers: {
Accept: "application/json",
},
})
.then((response) => response.json())
.catch((error) => {});
extConfig.selectors = result ?? extConfig.selectors;
console.log(result);
}
function initializeDisableVoteSubmission() {
getBrowser().storage.sync.get(["disableVoteSubmission"], (res) => {
if (res.disableVoteSubmission === undefined) {
getBrowser().storage.sync.set({ disableVoteSubmission: false });
} else {
extConfig.disableVoteSubmission = res.disableVoteSubmission;
}
});
}
function initializeDisableLogging() {
getBrowser().storage.sync.get(["disableLogging"], (res) => {
if (res.disableLogging === undefined) {
getBrowser().storage.sync.set({ disableLogging: true });
} else {
extConfig.disableLogging = res.disableLogging;
}
});
}
function initializeColoredThumbs() {
getBrowser().storage.sync.get(["coloredThumbs"], (res) => {
if (res.coloredThumbs === undefined) {
getBrowser().storage.sync.set({ coloredThumbs: false });
} else {
extConfig.coloredThumbs = res.coloredThumbs;
}
});
}
function initializeColoredBar() {
getBrowser().storage.sync.get(["coloredBar"], (res) => {
if (res.coloredBar === undefined) {
getBrowser().storage.sync.set({ coloredBar: false });
} else {
extConfig.coloredBar = res.coloredBar;
}
});
}
function initializeColorTheme() {
getBrowser().storage.sync.get(["colorTheme"], (res) => {
if (res.colorTheme === undefined) {
getBrowser().storage.sync.set({ colorTheme: false });
} else {
extConfig.colorTheme = res.colorTheme;
}
});
}
function initializeNumberDisplayFormat() {
getBrowser().storage.sync.get(["numberDisplayFormat"], (res) => {
if (res.numberDisplayFormat === undefined) {
getBrowser().storage.sync.set({ numberDisplayFormat: "compactShort" });
} else {
extConfig.numberDisplayFormat = res.numberDisplayFormat;
}
});
}
function initializeTooltipPercentage() {
getBrowser().storage.sync.get(["showTooltipPercentage"], (res) => {
if (res.showTooltipPercentage === undefined) {
getBrowser().storage.sync.set({ showTooltipPercentage: false });
} else {
extConfig.showTooltipPercentage = res.showTooltipPercentage;
}
});
}
function initializeTooltipPercentageMode() {
getBrowser().storage.sync.get(["tooltipPercentageMode"], (res) => {
if (res.tooltipPercentageMode === undefined) {
getBrowser().storage.sync.set({ tooltipPercentageMode: "dash_like" });
} else {
extConfig.tooltipPercentageMode = res.tooltipPercentageMode;
}
});
}
function initializeNumberDisplayReformatLikes() {
getBrowser().storage.sync.get(["numberDisplayReformatLikes"], (res) => {
if (res.numberDisplayReformatLikes === undefined) {
getBrowser().storage.sync.set({ numberDisplayReformatLikes: false });
} else {
extConfig.numberDisplayReformatLikes = res.numberDisplayReformatLikes;
}
});
}
export {
isMobile,
isShorts,
isVideoDisliked,
isVideoLiked,
isNewDesign,
isRoundedDesign,
getState,
setState,
setInitialState,
setLikes,
setDislikes,
getLikeCountFromButton,
LIKED_STATE,
DISLIKED_STATE,
NEUTRAL_STATE,
extConfig,
initExtConfig,
storedData,
isLikesDisabled,
};