Skip to content

Commit

Permalink
Add ability to set a delay between clicks
Browse files Browse the repository at this point in the history
Fix issue with disconnect
  • Loading branch information
AdamWr committed Mar 5, 2023
1 parent 61c1849 commit 727e00a
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 10 deletions.
6 changes: 5 additions & 1 deletion src/helpers/throttle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
52 changes: 43 additions & 9 deletions src/scriptlets/trusted-click-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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());
Expand All @@ -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;
Expand All @@ -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) {
Expand Down
185 changes: 185 additions & 0 deletions tests/scriptlets/trusted-click-element.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

0 comments on commit 727e00a

Please sign in to comment.