From 50cabb2ec575eaa05a21d4f512409301e41ea7b4 Mon Sep 17 00:00:00 2001 From: Elizaveta Egorova Date: Thu, 25 Jul 2024 13:35:18 +0300 Subject: [PATCH] =?UTF-8?q?AG-34532=20Improve=20'trusted-click-element'=20?= =?UTF-8?q?scriptlet=20=E2=80=94=20fix=20click=20on=20page=20layout=20elem?= =?UTF-8?q?ents.=20#437?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit 722b946ca51e0d4761849ffbcb13836e3bb00cae Merge: 46f05efe 2b397e10 Author: jellizaveta Date: Thu Jul 25 13:23:31 2024 +0300 Merge branch 'fix/AG-34532' of ssh://bit.int.agrd.dev:7999/adguard-filters/scriptlets into fix/AG-34532 commit 46f05efe6457e6c4953ee9b823c1085c22ca7038 Merge: 7cabd768 3b500cf4 Author: jellizaveta Date: Thu Jul 25 13:23:07 2024 +0300 Merge branch 'master' into fix/AG-34532 commit 2b397e10a37f14e18c356576a20c76521c923f18 Author: Slava Leleka Date: Wed Jul 24 18:48:07 2024 +0300 src/scriptlets/trusted-click-element.ts edited online with Bitbucket commit ebaa27e9b37778429ca45119d2183ef00aaf1a93 Author: Slava Leleka Date: Wed Jul 24 18:48:00 2024 +0300 CHANGELOG.md edited online with Bitbucket commit d986149e5528462152bf3b2b62ff5840a5df6656 Author: Slava Leleka Date: Wed Jul 24 18:47:49 2024 +0300 src/scriptlets/trusted-click-element.ts edited online with Bitbucket commit 7cabd76866de693de97b41cf927e73d15b417ea4 Author: jellizaveta Date: Wed Jul 24 13:25:06 2024 +0300 fix the click function of already existing elements in the DOM commit 99eb37dd73791b51a82f2b09a0c0f9108e0ab6d0 Author: jellizaveta Date: Tue Jul 23 18:40:13 2024 +0300 fix the case when one element is added before the scriptlet and the rest after commit 62fe248a9e49375ed834c7cf2d6c5b60d0792940 Author: jellizaveta Date: Tue Jul 23 17:55:04 2024 +0300 update tests and changelog commit 6bc3ed5f207431f1bdbe9d4ed7add97f9c8eb665 Author: jellizaveta Date: Tue Jul 23 15:30:21 2024 +0300 AG-34532 Improve 'trusted-click-element' scriptlet — fix click on page layout elements. #437 --- CHANGELOG.md | 5 ++ src/scriptlets/trusted-click-element.ts | 74 +++++++++++++++---- .../scriptlets/trusted-click-element.test.js | 60 ++++++++++++++- 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7842575dd..73cccfd88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,15 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic - new values to `set-cookie` and `set-cookie-reload` scriptlets: `essential`, `nonessential` [#436] - `trusted-set-session-storage-item` scriptlet [#426] +### Fixed + +- `trusted-click-element` scriptlet does not click on an element that is already in the DOM [#437] + [Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.11.6...HEAD [#435]: https://github.com/AdguardTeam/Scriptlets/issues/435 [#436]: https://github.com/AdguardTeam/Scriptlets/issues/436 [#426]: https://github.com/AdguardTeam/Scriptlets/issues/426 +[#437]: https://github.com/AdguardTeam/Scriptlets/issues/437 ## [v1.11.6] - 2024-07-08 diff --git a/src/scriptlets/trusted-click-element.ts b/src/scriptlets/trusted-click-element.ts index e739ff077..fc4c3430f 100644 --- a/src/scriptlets/trusted-click-element.ts +++ b/src/scriptlets/trusted-click-element.ts @@ -347,14 +347,12 @@ export function trustedClickElement( }; /** - * Query all selectors from queue on each mutation - * Each selector is swapped to null in selectorsSequence on founding corresponding element - * - * We start looking for elements before possible delay is over, to avoid cases - * when delay is getting off after the last mutation took place. + * Processes a sequence of selectors, handling elements found in DOM (and shadow DOM), + * and updates the sequence. * + * @returns {string[]} The updated selectors sequence, with fulfilled selectors set to null. */ - const findElements = (mutations: MutationRecord[], observer: MutationObserver) => { + const fulfillAndHandleSelectors = () => { const fulfilledSelectors: string[] = []; selectorsSequence.forEach((selector, i) => { if (!selector) { @@ -376,6 +374,20 @@ export function trustedClickElement( : selector; }); + return selectorsSequence; + }; + + /** + * Queries all selectors from queue on each mutation + * + * We start looking for elements before possible delay is over, to avoid cases + * when delay is getting off after the last mutation took place. + * + */ + const findElements = (mutations: MutationRecord[], observer: MutationObserver) => { + // TODO: try to make the function cleaner — avoid usage of selectorsSequence from the outer scope + selectorsSequence = fulfillAndHandleSelectors(); + // Disconnect observer after finding all elements const allSelectorsFulfilled = selectorsSequence.every((selector) => selector === null); if (allSelectorsFulfilled) { @@ -383,13 +395,49 @@ export function trustedClickElement( } }; - const observer = new MutationObserver(throttle(findElements, THROTTLE_DELAY_MS)); - observer.observe(document.documentElement, { - attributes: true, - childList: true, - subtree: true, - }); + /** + * Initializes a `MutationObserver` to watch for changes in the DOM. + * The observer is set up to monitor changes in attributes, child nodes, and subtree. + * A timeout is set to disconnect the observer if no elements are found within the specified time. + */ + const initializeMutationObserver = () => { + const observer = new MutationObserver(throttle(findElements, THROTTLE_DELAY_MS)); + observer.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true, + }); + + // Set timeout to disconnect observer if elements are not found within the specified time + setTimeout(() => observer.disconnect(), OBSERVER_TIMEOUT_MS); + }; + /** + * Checks if elements are already present in the DOM. + * If elements are found, they are clicked. + * If elements are not found, the observer is initialized. + */ + const checkInitialElements = () => { + const foundElements = selectorsSequence.every((selector) => { + if (!selector) { + return false; + } + const element = queryShadowSelector(selector); + return !!element; + }); + if (foundElements) { + // Click previously collected elements + fulfillAndHandleSelectors(); + } else { + // Initialize MutationObserver if elements were not found initially + initializeMutationObserver(); + } + }; + + // Run the initial check + checkInitialElements(); + + // If there's a delay before clicking elements, use a timeout if (parsedDelay) { setTimeout(() => { // Click previously collected elements @@ -397,8 +445,6 @@ export function trustedClickElement( canClick = true; }, parsedDelay); } - - setTimeout(() => observer.disconnect(), OBSERVER_TIMEOUT_MS); } trustedClickElement.names = [ diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js index 0e0a8cb5b..ef34baa3a 100644 --- a/tests/scriptlets/trusted-click-element.test.js +++ b/tests/scriptlets/trusted-click-element.test.js @@ -57,17 +57,37 @@ const afterEach = () => { module(name, { beforeEach, afterEach }); -test('Single element clicked', (assert) => { +test('Element already in DOM is clicked', (assert) => { const ELEM_COUNT = 1; - // Check elements for being clicked and hit func execution const ASSERTIONS = ELEM_COUNT + 1; assert.expect(ASSERTIONS); const done = assert.async(); const selectorsString = `#${PANEL_ID} > #${CLICKABLE_NAME}${ELEM_COUNT}`; + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); runScriptlet(name, [selectorsString]); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 150); +}); + +test('Element added to DOM is clicked', (assert) => { + const ELEM_COUNT = 1; + const ASSERTIONS = ELEM_COUNT + 1; + assert.expect(ASSERTIONS); + + const done = assert.async(); + const selectorsString = `#${PANEL_ID} > #${CLICKABLE_NAME}${ELEM_COUNT}`; const panel = createPanel(); + + runScriptlet(name, [selectorsString]); + const clickable = createClickable(1); panel.appendChild(clickable); @@ -78,6 +98,42 @@ test('Single element clicked', (assert) => { }, 150); }); +test('Multiple elements clicked - one element loaded before scriptlet, rest added later', (assert) => { + const CLICK_ORDER = [1, 2, 3]; + // Assert elements for being clicked, hit func execution & click order + const ASSERTIONS = CLICK_ORDER.length + 2; + assert.expect(ASSERTIONS); + const done = assert.async(); + + const selectorsString = createSelectorsString(CLICK_ORDER); + + const panel = createPanel(); + + const clickables = []; + const clickable1 = createClickable(1); + panel.appendChild(clickable1); + clickables.push(clickable1); + + runScriptlet(name, [selectorsString]); + + const clickable2 = createClickable(2); + panel.appendChild(clickable2); + clickables.push(clickable2); + + const clickable3 = createClickable(3); + panel.appendChild(clickable3); + clickables.push(clickable3); + + setTimeout(() => { + clickables.forEach((clickable) => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + }); + assert.strictEqual(CLICK_ORDER.join(), window.clickOrder.join(), 'Elements were clicked in a given order'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 400); +}); + test('Single element clicked, delay is set', (assert) => { const ELEM_COUNT = 1; const DELAY = 300;