From 727e00abc09912f85d9cdf4059c339100946829a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Sun, 5 Mar 2023 18:44:09 +0100 Subject: [PATCH] Add ability to set a delay between clicks Fix issue with disconnect --- src/helpers/throttle.js | 6 +- src/scriptlets/trusted-click-element.js | 52 ++++- .../scriptlets/trusted-click-element.test.js | 185 ++++++++++++++++++ 3 files changed, 233 insertions(+), 10 deletions(-) diff --git a/src/helpers/throttle.js b/src/helpers/throttle.js index 419c39ea..323b7ba6 100644 --- a/src/helpers/throttle.js +++ b/src/helpers/throttle.js @@ -21,7 +21,11 @@ export const throttle = (cb, delay) => { setTimeout(() => { wait = false; if (savedArgs) { - wrapper(savedArgs); + // it's necessary to use spread operator + // otherwise if there will be more than one argument + // then only one argument will be passed + // https://github.com/AdguardTeam/Scriptlets/issues/284#issuecomment-1419464354 + wrapper(...savedArgs); savedArgs = null; } }, delay); diff --git a/src/scriptlets/trusted-click-element.js b/src/scriptlets/trusted-click-element.js index f7617728..e7cadfbc 100644 --- a/src/scriptlets/trusted-click-element.js +++ b/src/scriptlets/trusted-click-element.js @@ -15,7 +15,7 @@ import { * * **Syntax** * ``` - * example.com#%#//scriptlet('trusted-click-element', selectors[, extraMatch[, delay]]) + * example.com#%#//scriptlet('trusted-click-element', selectors[, extraMatch[, delay[, sequenceDelay]]]) * ``` * * - `selectors` — required, string with query selectors delimited by comma @@ -24,6 +24,7 @@ import { * - `cookie` - test string or regex against cookies on a page * - `localStorage` - check if localStorage item is present * - `delay` — optional, time in ms to delay scriptlet execution, defaults to instant execution. + * - `sequenceDelay` — optional, time in ms to set delay between clicks, can be set for every element except first one, values separated by `|` * * **Examples** * 1. Click single element by selector @@ -60,9 +61,19 @@ import { * ``` * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], input[type="submit"][value="akkoord"]', 'cookie:cmpconsent, localStorage:promo', '250') * ``` + * + * 8. Click multiple elements by selector with a delay in seconds and third click + * ``` + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], button[name="check"], input[type="submit"][value="akkoord"]', '', '', '100|200') + * ``` + * + * 9. Click multiple elements by selector with a delay before first and next clicks + * ``` + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], button[name="check"], input[type="submit"][value="akkoord"]', '', '500', '2000|500') + * ``` */ /* eslint-enable max-len */ -export function trustedClickElement(source, selectors, extraMatch = '', delay = NaN) { +export function trustedClickElement(source, selectors, extraMatch = '', delay = NaN, sequenceDelay) { if (!selectors) { return; } @@ -75,19 +86,38 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = const COOKIE_STRING_DELIMITER = ';'; // Regex to split match pairs by commas, avoiding the ones included in regexes const EXTRA_MATCH_DELIMITER = /(,\s*){1}(?=cookie:|localStorage:)/; + const SEQUENCE_DELAY_DELIMITER = '|'; + + const delayBetweenSequencedClicks = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); + + const parseDelay = (delay) => parseInt(delay, 10); + const isValidDelay = (delay) => !Number.isNaN(delay) || delay < OBSERVER_TIMEOUT_MS; let parsedDelay; if (delay) { - parsedDelay = parseInt(delay, 10); - const isValidDelay = !Number.isNaN(parsedDelay) || parsedDelay < OBSERVER_TIMEOUT_MS; - if (!isValidDelay) { - // eslint-disable-next-line max-len + parsedDelay = parseDelay(delay); + if (!isValidDelay(parsedDelay)) { const message = `Passed delay '${delay}' is invalid or bigger than ${OBSERVER_TIMEOUT_MS} ms`; logMessage(source, message); return; } } + let parsedSequenceDelay; + if (sequenceDelay) { + parsedSequenceDelay = sequenceDelay + .split(SEQUENCE_DELAY_DELIMITER) + .map((delay) => parseDelay(delay)); + const areAllDelaysValid = parsedSequenceDelay + .every((delay) => isValidDelay(delay)); + if (!areAllDelaysValid) { + // eslint-disable-next-line max-len + const message = `Passed sequenceDelay '${sequenceDelay}' is invalid or bigger than ${OBSERVER_TIMEOUT_MS} ms`; + logMessage(source, message); + return; + } + } + let canClick = !parsedDelay; const cookieMatches = []; @@ -170,10 +200,11 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = .split(SELECTORS_DELIMITER) .map((selector) => selector.trim()); - const createElementObj = (element) => { + const createElementObj = (element, i) => { return { element: element || null, clicked: false, + delay: sequenceDelay ? parsedSequenceDelay[i - 1] : 0, }; }; const elementsSequence = Array(selectorsSequence.length).fill(createElementObj()); @@ -184,9 +215,12 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = * Element should not be clicked if it is already clicked, * or a previous element is not found or clicked yet */ - const clickElementsBySequence = () => { + const clickElementsBySequence = async () => { for (let i = 0; i < elementsSequence.length; i += 1) { const elementObj = elementsSequence[i]; + if (elementObj.delay) { + await delayBetweenSequencedClicks(elementObj.delay); + } // Stop clicking if that pos element is not found yet if (!elementObj.element) { break; @@ -207,7 +241,7 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = }; const handleElement = (element, i) => { - const elementObj = createElementObj(element); + const elementObj = createElementObj(element, i); elementsSequence[i] = elementObj; if (canClick) { diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js index 663f6062..bae33248 100644 --- a/tests/scriptlets/trusted-click-element.test.js +++ b/tests/scriptlets/trusted-click-element.test.js @@ -333,3 +333,188 @@ test('extraMatch - complex string+regex cookie input & whitespaces & comma in re }, 150); clearCookie(cookieKey1); }); + +test('Multiple elements clicked, sequence delay', (assert) => { + const SEQUENCE_DELAY = '100'; + // 100 ms + SEQUENCE_DELAY + const TIMEOUT_DELAY = 100 + parseInt(SEQUENCE_DELAY, 10); + const CLICK_ORDER = [1, 2, 3, 4]; + // 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); + + runScriptlet(name, [selectorsString, '', '', SEQUENCE_DELAY]); + const panel = createPanel(); + const clickables = []; + CLICK_ORDER.forEach((number) => { + const clickable = createClickable(number); + panel.appendChild(clickable); + clickables.push(clickable); + }); + + 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(); + }, TIMEOUT_DELAY); +}); + +test('Multiple elements clicked, delay + sequence delay', (assert) => { + const DELAY = 100; + const SEQUENCE_DELAY = '100|0|100'; + const CLICK_ORDER = [1, 2, 3, 4]; + // 100 ms + DELAY + sum of SEQUENCE_DELAY + const TIMEOUT_DELAY = 100 + DELAY + SEQUENCE_DELAY.split('|') + .map((item) => parseInt(item, 10)) + .reduce((accumulator, currentValue) => accumulator + currentValue, 0); + // 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); + + runScriptlet(name, [selectorsString, '', DELAY, SEQUENCE_DELAY]); + const panel = createPanel(); + const clickables = []; + CLICK_ORDER.forEach((number) => { + const clickable = createClickable(number); + panel.appendChild(clickable); + clickables.push(clickable); + }); + + 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(); + }, TIMEOUT_DELAY); +}); + +test('Multiple elements clicked, non-ordered render, sequence delay', (assert) => { + const SEQUENCE_DELAY = '100|50'; + const CLICK_ORDER = [2, 1, 3]; + // 100 ms + DELAY + sum of SEQUENCE_DELAY + const TIMEOUT_DELAY = 100 + SEQUENCE_DELAY.split('|') + .map((item) => parseInt(item, 10)) + .reduce((accumulator, currentValue) => accumulator + currentValue, 0); + // 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); + + runScriptlet(name, [selectorsString, '', '', SEQUENCE_DELAY]); + const panel = createPanel(); + const clickables = []; + CLICK_ORDER.forEach((number) => { + const clickable = createClickable(number); + panel.appendChild(clickable); + clickables.push(clickable); + }); + + 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(); + }, TIMEOUT_DELAY); +}); + +test('Multiple elements clicked, invalid sequence delay (invalid)', (assert) => { + const SEQUENCE_DELAY = 'invalid'; + const CLICK_ORDER = [1, 2, 3, 4]; + // Assert elements (4) should not be clicked and hit func should not execute + const ASSERTIONS = CLICK_ORDER.length + 1; + assert.expect(ASSERTIONS); + const done = assert.async(); + + const selectorsString = createSelectorsString(CLICK_ORDER); + + runScriptlet(name, [selectorsString, '', '', SEQUENCE_DELAY]); + const panel = createPanel(); + const clickables = []; + CLICK_ORDER.forEach((number) => { + const clickable = createClickable(number); + panel.appendChild(clickable); + clickables.push(clickable); + }); + + setTimeout(() => { + clickables.forEach((clickable) => { + assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); + }); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + done(); + }, 100); +}); + +test('Multiple elements clicked, invalid sequence delay (100|invalid|200)', (assert) => { + const SEQUENCE_DELAY = '100|invalid|200'; + const CLICK_ORDER = [1, 2, 3, 4]; + // Assert elements (4) should not be clicked and hit func should not execute + const ASSERTIONS = CLICK_ORDER.length + 1; + assert.expect(ASSERTIONS); + const done = assert.async(); + + const selectorsString = createSelectorsString(CLICK_ORDER); + + runScriptlet(name, [selectorsString, '', '', SEQUENCE_DELAY]); + const panel = createPanel(); + const clickables = []; + CLICK_ORDER.forEach((number) => { + const clickable = createClickable(number); + panel.appendChild(clickable); + clickables.push(clickable); + }); + + setTimeout(() => { + clickables.forEach((clickable) => { + assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); + }); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + done(); + }, 100); +}); + +// https://github.com/AdguardTeam/Scriptlets/issues/284#issuecomment-1419464354 +test('Test - wait for an element to click', (assert) => { + const ELEM_COUNT = 1; + const selectorsString = `#${PANEL_ID} > #${CLICKABLE_NAME}${ELEM_COUNT}`; + + assert.expect(1); + const done = assert.async(); + + runScriptlet(name, [selectorsString]); + + setTimeout(() => { + createPanel(); + }, 100); + + setTimeout(() => { + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + }, 101); + + setTimeout(() => { + createPanel(); + }, 102); + + setTimeout(() => { + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + removePanel(); + done(); + }, 200); +});