From 32102f60459d737207399227f2bc460819139fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Wr=C3=B3blewski?= Date: Tue, 28 Mar 2023 15:45:18 +0300 Subject: [PATCH] AG-20302 Add ability to click element if cookie/localStorage item doesn't exist. #298 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit 19d09f43e08047a3d3525e32e3beac20f629bd11 Author: Adam Wróblewski Date: Tue Mar 28 13:10:20 2023 +0200 Do not reassign function parameters commit 2b6fef7aabccf16ca51dcb998c4ebb8b0ba2f5e2 Author: Adam Wróblewski Date: Mon Mar 27 14:15:24 2023 +0200 Fix typo in description commit eaf048a54ceda6e03775212c8109acf1ecf8c218 Author: Adam Wróblewski Date: Mon Mar 27 11:56:13 2023 +0200 Use parseMatchArg helper Use native startsWith in parseMatchArg helper Return string in parseMatchArg helper Add test with cookie value Set isInvertedMatchCookie and isInvertedMatchLocalStorage to false instead of null Rever test for abort-on-stack-trace Fix typo in description commit df78bc3bba02709ed8b78c10398101f7236adaf9 Author: Adam Wróblewski Date: Tue Mar 21 20:17:52 2023 +0100 Add ability to click element if cookie/localStorage item doesn't exist --- CHANGELOG.md | 1 + src/helpers/string-utils.js | 5 +- src/scriptlets/trusted-click-element.js | 32 +- tests/scriptlets/abort-on-stack-trace.test.js | 2 +- .../scriptlets/trusted-click-element.test.js | 276 +++++++++++++++++- 5 files changed, 307 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e9501e..6d59071f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- ability for `trusted-click-element` scriptlet to click element if `cookie`/`localStorage` item doesn't exist [#298](https://github.com/AdguardTeam/Scriptlets/issues/298) - static delay between multiple clicks in `trusted-click-element` [#284](https://github.com/AdguardTeam/Scriptlets/issues/284) ### Fixed diff --git a/src/helpers/string-utils.js b/src/helpers/string-utils.js index cfc22e26..a03361a3 100644 --- a/src/helpers/string-utils.js +++ b/src/helpers/string-utils.js @@ -238,10 +238,11 @@ export const isValidMatchNumber = (match) => { */ export const parseMatchArg = (match) => { const INVERT_MARKER = '!'; - const isInvertedMatch = startsWith(match, INVERT_MARKER); + // In case if "match" is "undefined" return "false" + const isInvertedMatch = match ? match.startsWith(INVERT_MARKER) : false; const matchValue = isInvertedMatch ? match.slice(1) : match; const matchRegexp = toRegExp(matchValue); - return { isInvertedMatch, matchRegexp }; + return { isInvertedMatch, matchRegexp, matchValue }; }; /** diff --git a/src/scriptlets/trusted-click-element.js b/src/scriptlets/trusted-click-element.js index 5dd5a73b..16e5bd62 100644 --- a/src/scriptlets/trusted-click-element.js +++ b/src/scriptlets/trusted-click-element.js @@ -4,6 +4,7 @@ import { parseCookieString, throttle, logMessage, + parseMatchArg, } from '../helpers/index'; /* eslint-disable max-len */ @@ -20,6 +21,7 @@ import { * * - `selectors` — required, string with query selectors delimited by comma * - `extraMatch` — optional, extra condition to check on a page; allows to match `cookie` and `localStorage`; can be set as `name:key[=value]` where `value` is optional. + * If `cookie`/`localStorage` starts with `!` then the element will only be clicked if specified cookie/localStorage item does not exist. * Multiple conditions are allowed inside one `extraMatch` but they should be delimited by comma and each of them should match the syntax. Possible `name`s: * - `cookie` - test string or regex against cookies on a page * - `localStorage` - check if localStorage item is present @@ -60,6 +62,16 @@ import { * ``` * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], input[type="submit"][value="akkoord"]', 'cookie:cmpconsent, localStorage:promo', '250') * ``` + * + * 8. Click element only if cookie with name `cmpconsent` does not exist + * ``` + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', '!cookie:cmpconsent') + * ``` + * + * 9. Click element only if specified cookie string and localStorage item does not exist + * ``` + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', '!cookie:cmpconsent, !localStorage:promo') + * ``` */ /* eslint-enable max-len */ export function trustedClickElement(source, selectors, extraMatch = '', delay = NaN) { @@ -75,7 +87,7 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = const SELECTORS_DELIMITER = ','; 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 EXTRA_MATCH_DELIMITER = /(,\s*){1}(?=!?cookie:|!?localStorage:)/; const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); @@ -95,6 +107,8 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = const cookieMatches = []; const localStorageMatches = []; + let isInvertedMatchCookie = false; + let isInvertedMatchLocalStorage = false; if (extraMatch) { // Get all match marker:value pairs from argument @@ -105,11 +119,15 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = // Filter match pairs by marker parsedExtraMatch.forEach((matchStr) => { if (matchStr.indexOf(COOKIE_MATCH_MARKER) > -1) { - const cookieMatch = matchStr.replace(COOKIE_MATCH_MARKER, ''); + const { isInvertedMatch, matchValue } = parseMatchArg(matchStr); + isInvertedMatchCookie = isInvertedMatch; + const cookieMatch = matchValue.replace(COOKIE_MATCH_MARKER, ''); cookieMatches.push(cookieMatch); } if (matchStr.indexOf(LOCAL_STORAGE_MATCH_MARKER) > -1) { - const localStorageMatch = matchStr.replace(LOCAL_STORAGE_MATCH_MARKER, ''); + const { isInvertedMatch, matchValue } = parseMatchArg(matchStr); + isInvertedMatchLocalStorage = isInvertedMatch; + const localStorageMatch = matchValue.replace(LOCAL_STORAGE_MATCH_MARKER, ''); localStorageMatches.push(localStorageMatch); } }); @@ -145,7 +163,8 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = }); }); - if (!cookiesMatched) { + const shouldRun = cookiesMatched !== isInvertedMatchCookie; + if (!shouldRun) { return; } } @@ -156,7 +175,9 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = const itemValue = window.localStorage.getItem(str); return itemValue || itemValue === ''; }); - if (!localStorageMatched) { + + const shouldRun = localStorageMatched !== isInvertedMatchLocalStorage; + if (!shouldRun) { return; } } @@ -287,4 +308,5 @@ trustedClickElement.injections = [ parseCookieString, throttle, logMessage, + parseMatchArg, ]; diff --git a/tests/scriptlets/abort-on-stack-trace.test.js b/tests/scriptlets/abort-on-stack-trace.test.js index 9a5835d0..6e7a35d6 100644 --- a/tests/scriptlets/abort-on-stack-trace.test.js +++ b/tests/scriptlets/abort-on-stack-trace.test.js @@ -346,7 +346,7 @@ test('do NOT abort Math.round, test for injected script', (assert) => { // eslint-disable-next-line no-console console.log('Something went wrong', error); } - assert.strictEqual(testPassed, true, 'testPassed set to true, script has been aborted'); + assert.strictEqual(testPassed, true, 'testPassed set to true, script has not been aborted'); assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); }); diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js index 8bc122a2..66838835 100644 --- a/tests/scriptlets/trusted-click-element.test.js +++ b/tests/scriptlets/trusted-click-element.test.js @@ -38,7 +38,8 @@ const createPanel = () => { const removePanel = () => document.getElementById('panel').remove(); const clearCookie = (cName) => { - document.cookie = `${cName}=; max-age=0`; + // Without "path=/;" cookie is not removed + document.cookie = `${cName}=; path=/; max-age=0`; }; const beforeEach = () => { @@ -375,6 +376,279 @@ test('extraMatch - complex string+regex cookie input & whitespaces & comma in re clearCookie(cookieKey1); }); +test('extraMatch - single cookie match + single localStorage match, matched', (assert) => { + const cookieKey1 = 'cookieMatch'; + const cookieData = concatCookieNameValuePath(cookieKey1, 'true', '/'); + document.cookie = cookieData; + const itemName = 'itemMatch'; + window.localStorage.setItem(itemName, 'value'); + const EXTRA_MATCH_STR = `cookie:${cookieKey1}, localStorage:${itemName}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 150); + clearCookie(cookieKey1); + window.localStorage.clear(); +}); + +test('extraMatch - single cookie revert, click', (assert) => { + const cookieKey = 'revertTest'; + const EXTRA_MATCH_STR = `!cookie:${cookieKey}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 150); +}); + +test('extraMatch - single cookie with value revert match, should click', (assert) => { + const cookieKey = 'clickValue'; + const cookieVal = 'true'; + const cookieData = concatCookieNameValuePath(cookieKey, cookieVal, '/'); + document.cookie = cookieData; + const EXTRA_MATCH_STR = `!cookie:${cookieKey}=false`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + clearCookie(cookieKey); + done(); + }, 150); +}); + +test('extraMatch - single cookie revert match, should not click', (assert) => { + const cookieKey = 'doNotClick'; + const cookieData = concatCookieNameValuePath(cookieKey, 'true', '/'); + document.cookie = cookieData; + const EXTRA_MATCH_STR = `!cookie:${cookieKey}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + clearCookie(cookieKey); + done(); + }, 150); +}); + +test('extraMatch - single cookie with value revert match, should not click', (assert) => { + const cookieKey = 'doNotClickValue'; + const cookieVal = 'true'; + const cookieData = concatCookieNameValuePath(cookieKey, cookieVal, '/'); + document.cookie = cookieData; + const EXTRA_MATCH_STR = `!cookie:${cookieKey}=${cookieVal}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + clearCookie(cookieKey); + done(); + }, 150); +}); + +test('extraMatch - single localStorage revert, click', (assert) => { + const itemName = 'revertItem'; + const EXTRA_MATCH_STR = `!localStorage:${itemName}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 150); +}); + +test('extraMatch - single localStorage revert match, should not click', (assert) => { + const itemName = 'revertItem2'; + window.localStorage.setItem(itemName, 'value'); + const EXTRA_MATCH_STR = `!localStorage:${itemName}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + done(); + }, 150); + window.localStorage.clear(); +}); + +test('extraMatch - single cookie match + single localStorage match, revert - click', (assert) => { + const cookieKey1 = 'cookieRevertAndItem'; + const itemName = 'itemRevertAndCookie'; + const EXTRA_MATCH_STR = `!cookie:${cookieKey1}, !localStorage:${itemName}`; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 150); +}); + +test('extraMatch - complex string+regex cookie input&whitespaces&comma in regex, revert should not click', (assert) => { + const cookieKey1 = 'first'; + const cookieVal1 = 'true'; + const cookieData1 = concatCookieNameValuePath(cookieKey1, cookieVal1, '/'); + const cookieKey2 = 'sec'; + const cookieVal2 = '1-1'; + const cookieData2 = concatCookieNameValuePath(cookieKey2, cookieVal2, '/'); + const cookieKey3 = 'third'; + const cookieVal3 = 'true'; + const cookieData3 = concatCookieNameValuePath(cookieKey3, cookieVal3, '/'); + + document.cookie = cookieData1; + document.cookie = cookieData2; + document.cookie = cookieData3; + + const EXTRA_MATCH_STR = '!cookie:/firs/=true,cookie:sec=/(1-1){1,2}/, !cookie:third=true'; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + done(); + }, 150); + clearCookie(cookieKey1); +}); + +test('extraMatch - complex string+regex cookie input&whitespaces&comma in regex, revert should click', (assert) => { + const EXTRA_MATCH_STR = '!cookie:/firs/=true,cookie:sec=/(1-1){1,2}/, !cookie:third=true'; + + 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}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); + done(); + }, 150); +}); + // https://github.com/AdguardTeam/Scriptlets/issues/284#issuecomment-1419464354 test('Test - wait for an element to click', (assert) => { const ELEM_COUNT = 1;