Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update share button query #52

Merged
merged 10 commits into from
Dec 23, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ extension.zip
test-results
playwright-report
playwright/.cache
.DS_Store
14 changes: 8 additions & 6 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { defineConfig, devices } from "@playwright/test";

const CI = process.env.CI;

/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: "src/tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
workers: process.env.CI ? 1 : undefined,
forbidOnly: !!CI,
workers: CI ? 1 : undefined,
reporter: [["html", { open: "never" }]],
timeout: 60000,
// use: {
// trace: "retain-on-failure",
// },
timeout: CI ? 120000 : 60000,
use: {
trace: "retain-on-failure",
},
projects: [
{
name: "chromium",
Expand Down
2 changes: 2 additions & 0 deletions src/constants/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ export const langToEndAtStringMap = new Map([
["pt-BR", "Terminar em"],
["tr-TR", "Son:"],
]);

export const syncSleepTime = 400;
7 changes: 4 additions & 3 deletions src/constants/utils/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const endAtContainerID = "end-at";
export const sleepTime = 200;
export const pollingTimeoutInSeconds = 30;

export const shareIconParentSelector = "#actions-inner";
export const shareIconPathSelector =
'path[d="M15 5.63 20.66 12 15 18.37V14h-1c-3.96 0-7.14 1-9.75 3.09 1.84-4.07 5.11-6.4 9.89-7.1l.86-.13V5.63M14 3v6C6.22 10.13 3.11 15.33 2 21c2.78-3.97 6.44-6 12-6v6l8-9-8-9z"]';
export const shareButtonSelector =
"#actions-inner #top-level-buttons-computed ytd-button-renderer button";
export const shareButtonSelector2 =
"#actions-inner #top-level-buttons-computed yt-button-view-model button";
9 changes: 3 additions & 6 deletions src/extension.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
defaultEndAtLabelText,
langToEndAtStringMap,
syncSleepTime,
} from "./constants/extension.js";
import { endAtContainerID } from "./constants/utils/queries.js";
import {
Expand All @@ -21,7 +22,6 @@ import {
getShareDialog,
getStartAtCloneLabelElement,
getBody,
getShareIcon,
} from "./utils/queries.js";

/**
Expand Down Expand Up @@ -244,7 +244,7 @@ const removeEndAtContainer = (startAtContainer) => {
const onShareButtonClick = async () => {
// This delay is used because this part of the DOM is changed by YouTube as well.
// Allow some time for Youtube's changes to be applied first.
await sleep(400);
await sleep(syncSleepTime);

let shareDialog = await getShareDialog();
if (!shareDialog) return logElementNotFoundError("share dialog");
Expand All @@ -262,10 +262,7 @@ const onShareButtonClick = async () => {
};

const addOnShareButtonClickListener = async () => {
let shareIcon = await getShareIcon();
if (!shareIcon) return logElementNotFoundError("share icon");

let shareButton = await getShareButton(shareIcon);
let shareButton = await getShareButton();
if (!shareButton) return logElementNotFoundError("share button");

shareButton.addEventListener("click", onShareButtonClick);
Expand Down
2 changes: 2 additions & 0 deletions src/tests/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const maxRetries = 60;
export const sleepTime = 4000;
export const singleActionTimeout = 5000;
export const youtubeLandingPage = "https://www.youtube.com/";
export const youtubeTestVideoPage =
"https://www.youtube.com/watch?v=Czvldzei4DI";
Expand Down
42 changes: 30 additions & 12 deletions src/tests/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { errors } from "@playwright/test";
import { expect } from "./fixtures.js";
import {
endAtContainerID,
shareIconParentSelector,
shareIconPathSelector,
shareButtonSelector,
shareButtonSelector2,
startAtContainerID,
} from "../constants/utils/queries.js";
import {
languageIconPathSelector,
maxRetries,
menuIconPathSelector,
searchIconPathSelector,
singleActionTimeout,
sleepTime,
testVideoSearchTerm,
testVideoTitle,
} from "./constants.js";
Expand All @@ -29,7 +31,7 @@ const doUntil = async (callback, condition) => {
while (!condition() && retriesLeft > 0) {
await callback();
retriesLeft--;
sleep(4000);
sleep(sleepTime);
}
if (!condition() && retriesLeft === 0)
console.warn("No retries left and condition is not satisfied.");
Expand All @@ -52,9 +54,7 @@ export const rejectCookies = async (page) => {
name: "Reject the use of cookies and other data for the purposes described",
});
try {
await rejectButton.click({
timeout: 5000,
});
await rejectButton.click({ timeout: singleActionTimeout });
} catch (error) {
if (error instanceof errors.TimeoutError)
console.info("Reject cookies button not found.");
Expand Down Expand Up @@ -84,7 +84,6 @@ export const searchForVideo = async (page) => {
*/
const isOnPage = async (page, pathPrefix) => {
await page.waitForURL((url) => url.pathname.startsWith(pathPrefix));
await page.waitForTimeout(5000);
};

/**
Expand Down Expand Up @@ -115,11 +114,30 @@ export const clickOnAVideo = async (page) => {
* @param {Page} page
*/
const clickShareButton = async (page) => {
let shareIconParent = page.locator(shareIconParentSelector);
let shareButton = shareIconParent.locator("button", {
has: page.locator(shareIconPathSelector),
});
await shareButton.click();
for (;;) {
let shareButton = page.locator(shareButtonSelector);
try {
await shareButton.click({ timeout: singleActionTimeout });
return;
} catch (error) {
if (error instanceof errors.TimeoutError) {
console.warn(
`Share button not found with selector ${shareButtonSelector}.`,
);
}
}
shareButton = page.locator(shareButtonSelector2);
try {
await shareButton.click({ timeout: singleActionTimeout });
return;
} catch (error) {
if (error instanceof errors.TimeoutError) {
console.warn(
`Share button not found with selector ${shareButtonSelector2}.`,
);
}
}
}
};

/**
Expand Down
80 changes: 37 additions & 43 deletions src/utils/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { sleep } from "./other.js";
import {
endAtContainerID,
pollingTimeoutInSeconds,
shareIconParentSelector,
shareIconPathSelector,
shareButtonSelector,
shareButtonSelector2,
sleepTime,
startAtContainerID,
} from "../constants/utils/queries.js";
Expand All @@ -16,14 +16,15 @@ import {

/**
* @template T
* @param {() => T?} elementGetter
* @param {(() => T?)[]} elementGetters
* @returns {Promise<T?>}
*/
const pollForElement = async (elementGetter) => {
const pollForElement = async (elementGetters) => {
const pollsPerSecond = 1000 / sleepTime;
const numberOfPolls = pollsPerSecond * pollingTimeoutInSeconds;

for (let i = 0; i < numberOfPolls; i++) {
let elementGetter = elementGetters[i % elementGetters.length];
let element = elementGetter();
if (element) return element;
await sleep(sleepTime);
Expand All @@ -36,89 +37,82 @@ const pollForElement = async (elementGetter) => {
* @type {InputElementGetter}
*/
export const getStartAtInputElement = async () =>
await pollForElement(() =>
document.querySelector(`#${startAtContainerID} input`),
);
await pollForElement([
() => document.querySelector(`#${startAtContainerID} input`),
]);

/**
* @type {InputElementGetter}
*/
export const getEndAtInputElement = async () =>
await pollForElement(() =>
document.querySelector(`#${endAtContainerID} input`),
);
await pollForElement([
() => document.querySelector(`#${endAtContainerID} input`),
]);

/**
* @type {CheckboxElementGetter}
*/
export const getStartAtCheckboxElement = async () =>
await pollForElement(() =>
document.querySelector(`#${startAtContainerID} #start-at-checkbox`),
);
await pollForElement([
() => document.querySelector(`#${startAtContainerID} #start-at-checkbox`),
]);

/**
* @type {CheckboxElementGetter}
*/
export const getEndAtCheckboxElement = async () =>
await pollForElement(() =>
document.querySelector(`#${endAtContainerID} #start-at-checkbox`),
);
await pollForElement([
() => document.querySelector(`#${endAtContainerID} #start-at-checkbox`),
]);

/**
* @type {InputElementGetter}
*/
export const getShareURLElement = async () =>
await pollForElement(
await pollForElement([
() =>
/** @type {HTMLInputElement} */ (document.getElementById("share-url")),
);
]);

/**
* @type {ElementGetter}
*/
export const getStartAtContainer = async () =>
await pollForElement(() =>
document.querySelector(
`ytd-popup-container #contents #${startAtContainerID}`,
),
);
await pollForElement([
() =>
document.querySelector(
`ytd-popup-container #contents #${startAtContainerID}`,
),
]);

/**
* @param {Element} nextElement
* @returns {Promise<Element?>}
*/
export const getStartAtCloneLabelElement = async (nextElement) =>
await pollForElement(() =>
nextElement.querySelector("#checkboxLabel yt-formatted-string"),
);
await pollForElement([
() => nextElement.querySelector("#checkboxLabel yt-formatted-string"),
]);

/**
* @type {ElementGetter}
*/
export const getShareDialog = async () =>
await pollForElement(() =>
document.querySelector("ytd-popup-container #contents"),
);
await pollForElement([
() => document.querySelector("ytd-popup-container #contents"),
]);

/**
* @type {ElementGetter}
*/
export const getShareIcon = async () =>
await pollForElement(() =>
document.querySelector(
`${shareIconParentSelector} ${shareIconPathSelector}`,
),
);

/**
* @param {Element} shareIcon
* @returns {Promise<Element?>}
*/
export const getShareButton = async (shareIcon) =>
await pollForElement(() => shareIcon.closest("button"));
export const getShareButton = async () =>
await pollForElement([
() => document.querySelector(shareButtonSelector),
() => document.querySelector(shareButtonSelector2),
]);

/**
* @type {ElementGetter}
*/
export const getBody = async () =>
await pollForElement(() => document.querySelector("body"));
await pollForElement([() => document.querySelector("body")]);