Skip to content

Commit

Permalink
Update share button query (#52)
Browse files Browse the repository at this point in the history
* fix: revert share button selector

* test: remove timeouts

* test: enable trace on first retry

* feat: add .DS_Store to .gitignore

* test: increase timeout on CI

* test: refactor playwright config

* test: do not retry, record trace

* feat(queries): use many getters in pollForElement()

* test: use fallback query to find share button

* refactor: extract sync sleep time to a constant
  • Loading branch information
elias-pap authored Dec 23, 2023
1 parent 1a2b796 commit 86e7e36
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 70 deletions.
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")]);

0 comments on commit 86e7e36

Please sign in to comment.