Skip to content

Commit d12f6bf

Browse files
committed
AG-38180 Improve 'trusted-click-element' — check for 'containsText' of all matched selectors. #468
Squashed commit of the following: commit e050074 Author: Adam Wróblewski <[email protected]> Date: Tue Dec 3 09:19:03 2024 +0100 Update docs Rename `element` to `selector` commit c773ffd Author: Slava Leleka <[email protected]> Date: Tue Dec 3 11:05:51 2024 +0300 src/helpers/open-shadow-dom-utils.ts edited online with Bitbucket commit a990771 Author: Adam Wróblewski <[email protected]> Date: Mon Dec 2 14:36:52 2024 +0100 Improve 'trusted-click-element' — check for 'containsText' of all matched selectors
1 parent eb2f95b commit d12f6bf

File tree

4 files changed

+128
-28
lines changed

4 files changed

+128
-28
lines changed

Diff for: CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1212

1313
## [Unreleased]
1414

15+
### Changed
16+
17+
- `trusted-click-element` scriptlet, now when `containsText` is used then it will search for all given selectors
18+
and click on the first element with matched text [#468]
19+
1520
### Fixed
1621

1722
- issue with `trusted-click-element` scriptlet when `delay` was used and the element was removed
1823
and added again before it was clicked [#391]
1924

2025
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.0.1...HEAD
2126
[#391]: https://github.com/AdguardTeam/Scriptlets/issues/391
27+
[#468]: https://github.com/AdguardTeam/Scriptlets/issues/468
2228

2329
## [v2.0.1] - 2024-11-13
2430

Diff for: src/helpers/open-shadow-dom-utils.ts

+51-5
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { flatten } from './array-utils';
99
export const findHostElements = (rootElement: Element | ShadowRoot | null): HTMLElement[] => {
1010
const hosts: HTMLElement[] = [];
1111
if (rootElement) {
12-
// Element.querySelectorAll() returns list of elements
13-
// which are defined in DOM of Element.
14-
// Meanwhile, inner DOM of the element with shadowRoot property
15-
// is absolutely another DOM and which can not be reached by querySelectorAll('*')
12+
// Element.querySelectorAll() returns list of elements
13+
// which are defined in DOM of Element.
14+
// Meanwhile, inner DOM of the element with shadowRoot property
15+
// is absolutely another DOM and which can not be reached by querySelectorAll('*')
1616
const domElems = rootElement.querySelectorAll('*');
1717
domElems.forEach((el) => {
1818
if (el.shadowRoot) {
@@ -73,6 +73,47 @@ export const pierceShadowDom = (
7373

7474
type QueryFunc = typeof document.querySelector;
7575

76+
/**
77+
* Checks if an element contains the specified text.
78+
*
79+
* @param element - The element to check.
80+
* @param matchRegexp - The text to match.
81+
* @returns True if the element contains the specified text, otherwise false.
82+
*/
83+
export function doesElementContainText(
84+
element: Element,
85+
matchRegexp: RegExp,
86+
): boolean {
87+
const { textContent } = element;
88+
if (!textContent) {
89+
return false;
90+
}
91+
return matchRegexp.test(textContent);
92+
}
93+
94+
/**
95+
* Finds an element within the given root element that matches the specified element
96+
* and contains text matching the provided regular expression.
97+
*
98+
* @param rootElement - The root element to search within.
99+
* @param selector - The element to find.
100+
* @param matchRegexp - The regular expression to match the text content of the elements.
101+
* @returns The first element that matches the criteria, or null if no such element is found.
102+
*/
103+
export function findElementWithText(
104+
rootElement: Element,
105+
selector: string,
106+
matchRegexp: RegExp,
107+
): Element | null {
108+
const elements = rootElement.querySelectorAll(selector);
109+
for (let i = 0; i < elements.length; i += 1) {
110+
if (doesElementContainText(elements[i], matchRegexp)) {
111+
return elements[i];
112+
}
113+
}
114+
return null;
115+
}
116+
76117
/**
77118
* Retrieves the first Element that matches the selector, with the ability
78119
* to select elements from inside open shadow-dom.
@@ -82,15 +123,20 @@ type QueryFunc = typeof document.querySelector;
82123
* to find the element containing shadow root, and shadow root selector, to find the element inside shadow dom.
83124
* @param context The Element or Document which is the context for the query.
84125
* @param context.querySelector The querySelector function to use.
126+
* @param textContent The text content to match.
85127
* @returns The first Element within the document that matches the specified selector, or null if no matches are found.
86128
*/
87129
export function queryShadowSelector(
88130
selector: string,
89131
context: { querySelector: QueryFunc } = document.documentElement,
132+
textContent: RegExp | null = null,
90133
): ReturnType<QueryFunc> {
91134
const SHADOW_COMBINATOR = ' >>> ';
92135
const pos = selector.indexOf(SHADOW_COMBINATOR);
93136
if (pos === -1) {
137+
if (textContent) {
138+
return findElementWithText(context as Element, selector, textContent);
139+
}
94140
return context.querySelector(selector);
95141
}
96142

@@ -101,5 +147,5 @@ export function queryShadowSelector(
101147
}
102148

103149
const shadowRootSelector = selector.slice(pos + SHADOW_COMBINATOR.length).trim();
104-
return queryShadowSelector(shadowRootSelector, elem.shadowRoot);
150+
return queryShadowSelector(shadowRootSelector, elem.shadowRoot, textContent);
105151
}

Diff for: src/scriptlets/trusted-click-element.ts

+9-23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
logMessage,
77
parseMatchArg,
88
queryShadowSelector,
9+
doesElementContainText,
10+
findElementWithText,
911
} from '../helpers';
1012
import { type Source } from './scriptlets';
1113

@@ -16,6 +18,9 @@ import { type Source } from './scriptlets';
1618
* @description
1719
* Clicks selected elements in a strict sequence, ordered by selectors passed,
1820
* and waiting for them to render in the DOM first.
21+
* First matched element is clicked unless `containsText` is specified.
22+
* If `containsText` is specified, then it searches for all given selectors and clicks
23+
* the first element containing the specified text.
1924
* Deactivates after all elements have been clicked or by 10s timeout.
2025
*
2126
* ### Syntax
@@ -304,24 +309,6 @@ export function trustedClickElement(
304309

305310
const textMatchRegexp = textMatches ? toRegExp(textMatches) : null;
306311

307-
/**
308-
* Checks if an element contains the specified text.
309-
*
310-
* @param element - The element to check.
311-
* @param matchRegexp - The text to match.
312-
* @returns True if the element contains the specified text, otherwise false.
313-
*/
314-
const doesElementContainText = (
315-
element: Element,
316-
matchRegexp: RegExp,
317-
): boolean => {
318-
const { textContent } = element;
319-
if (!textContent) {
320-
return false;
321-
}
322-
return matchRegexp.test(textContent);
323-
};
324-
325312
/**
326313
* Create selectors array and swap selectors to null on finding it's element
327314
*
@@ -430,9 +417,6 @@ export function trustedClickElement(
430417

431418
// Skip already clicked elements
432419
if (!elementObj.clicked) {
433-
if (textMatchRegexp && !doesElementContainText(elementObj.element, textMatchRegexp)) {
434-
continue;
435-
}
436420
// Checks if node is connected to a Document object,
437421
// if not, try to find the element again
438422
// https://github.com/AdguardTeam/Scriptlets/issues/391
@@ -480,7 +464,7 @@ export function trustedClickElement(
480464
if (!selector) {
481465
return;
482466
}
483-
const element = queryShadowSelector(selector);
467+
const element = queryShadowSelector(selector, document.documentElement, textMatchRegexp);
484468
if (!element) {
485469
return;
486470
}
@@ -544,7 +528,7 @@ export function trustedClickElement(
544528
if (!selector) {
545529
return false;
546530
}
547-
const element = queryShadowSelector(selector);
531+
const element = queryShadowSelector(selector, document.documentElement, textMatchRegexp);
548532
return !!element;
549533
});
550534
if (foundElements) {
@@ -585,4 +569,6 @@ trustedClickElement.injections = [
585569
logMessage,
586570
parseMatchArg,
587571
queryShadowSelector,
572+
doesElementContainText,
573+
findElementWithText,
588574
];

Diff for: tests/scriptlets/trusted-click-element.test.js

+62
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,35 @@ test('extraMatch - text match, matched', (assert) => {
316316
}, 150);
317317
});
318318

319+
test('extraMatch - text match, few elements, matched only first element with text', (assert) => {
320+
const textToMatch = 'Accept cookie';
321+
const EXTRA_MATCH_STR = `containsText:${textToMatch}`;
322+
323+
const ELEM_COUNT = 1;
324+
// Check hit func execution, one element should be clicked, and one should not be clicked (3)
325+
const ASSERTIONS = ELEM_COUNT + 1 + 1;
326+
assert.expect(ASSERTIONS);
327+
const done = assert.async();
328+
329+
const selectorsString = `#${PANEL_ID} > [id^="${CLICKABLE_NAME}"]`;
330+
331+
runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]);
332+
const panel = createPanel();
333+
const clickableNotMatched = createClickable(1, 'Not match');
334+
const clickableMatched = createClickable(1, textToMatch);
335+
const clickableMatchedShouldNotBeClicked = createClickable(1, textToMatch);
336+
panel.appendChild(clickableNotMatched);
337+
panel.appendChild(clickableMatched);
338+
panel.appendChild(clickableMatchedShouldNotBeClicked);
339+
340+
setTimeout(() => {
341+
assert.ok(clickableMatched.getAttribute('clicked'), 'Element should be clicked');
342+
assert.notOk(clickableMatchedShouldNotBeClicked.getAttribute('clicked'), 'Element should NOT be clicked');
343+
assert.strictEqual(window.hit, 'FIRED', 'hit func executed');
344+
done();
345+
}, 150);
346+
});
347+
319348
test('extraMatch - text match regexp, matched', (assert) => {
320349
const textToMatch = 'Reject foo bar cookie';
321350
const EXTRA_MATCH_STR = 'containsText:/Reject.*cookie/';
@@ -894,3 +923,36 @@ test('Closed shadow dom element clicked', (assert) => {
894923
done();
895924
}, 150);
896925
});
926+
927+
test('Closed shadow dom element clicked - text', (assert) => {
928+
const textToMatch = 'Accept cookie';
929+
const EXTRA_MATCH_STR = `containsText:${textToMatch}`;
930+
931+
const ELEM_COUNT = 1;
932+
// Check hit func execution, one element should be clicked, and one should not be clicked (3)
933+
const ASSERTIONS = ELEM_COUNT + 1 + 1;
934+
assert.expect(ASSERTIONS);
935+
const done = assert.async();
936+
937+
const selectorsString = `#${PANEL_ID} >>> div > [id^="${CLICKABLE_NAME}"]`;
938+
939+
runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]);
940+
941+
const panel = createPanel();
942+
const shadowRoot = panel.attachShadow({ mode: 'closed' });
943+
const div = document.createElement('div');
944+
const clickableNotMatched = createClickable(1, 'Not match');
945+
const clickableMatched = createClickable(1, textToMatch);
946+
const clickableMatchedShouldNOTBeClicked = createClickable(1, textToMatch);
947+
div.appendChild(clickableNotMatched);
948+
div.appendChild(clickableMatched);
949+
div.appendChild(clickableMatchedShouldNOTBeClicked);
950+
shadowRoot.appendChild(div);
951+
952+
setTimeout(() => {
953+
assert.ok(clickableMatched.getAttribute('clicked'), 'Element should be clicked');
954+
assert.notOk(clickableMatchedShouldNOTBeClicked.getAttribute('clicked'), 'Element should NOT be clicked');
955+
assert.strictEqual(window.hit, 'FIRED', 'hit func executed');
956+
done();
957+
}, 150);
958+
});

0 commit comments

Comments
 (0)