@duckduckgo/autoconsent 8.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/build.sh +1 -0
  3. package/dist/addon-firefox/background.bundle.js +45 -42
  4. package/dist/addon-firefox/content.bundle.js +468 -380
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-mv3/background.bundle.js +45 -42
  7. package/dist/addon-mv3/content.bundle.js +468 -380
  8. package/dist/addon-mv3/manifest.json +1 -1
  9. package/dist/addon-mv3/popup.bundle.js +71 -33
  10. package/dist/addon-mv3/popup.html +28 -0
  11. package/dist/autoconsent.cjs.js +468 -380
  12. package/dist/autoconsent.esm.js +468 -380
  13. package/dist/autoconsent.playwright.js +1 -1
  14. package/dist/autoconsent.unit.js +10370 -0
  15. package/lib/cmps/airbnb.ts +5 -6
  16. package/lib/cmps/base.ts +97 -41
  17. package/lib/cmps/consentmanager.ts +13 -14
  18. package/lib/cmps/conversant.ts +8 -9
  19. package/lib/cmps/cookiebot.ts +8 -9
  20. package/lib/cmps/evidon.ts +7 -8
  21. package/lib/cmps/klaro.ts +13 -14
  22. package/lib/cmps/onetrust.ts +15 -16
  23. package/lib/cmps/sourcepoint-frame.ts +25 -26
  24. package/lib/cmps/tiktok.ts +7 -7
  25. package/lib/cmps/trustarc-frame.ts +27 -28
  26. package/lib/cmps/trustarc-top.ts +5 -6
  27. package/lib/cmps/uniconsent.ts +9 -10
  28. package/lib/dom-actions.ts +145 -0
  29. package/lib/types.ts +24 -1
  30. package/lib/utils.ts +32 -1
  31. package/lib/web.ts +46 -34
  32. package/package.json +4 -4
  33. package/playwright/runner.ts +11 -3
  34. package/playwright/unit.ts +15 -0
  35. package/readme.md +1 -1
  36. package/tests/{rule-executors.spec.ts → dom-actions.spec.ts} +20 -21
  37. package/lib/config.ts +0 -2
  38. package/lib/rule-executors.ts +0 -147
@@ -1,5 +1,3 @@
1
- import { enableLogs } from "../config";
2
- import { click, elementExists, wait, waitForElement, waitForThenClick } from "../rule-executors";
3
1
  import { RunContext } from "../rules";
4
2
  import { waitFor } from "../utils";
5
3
  import AutoConsentCMPBase from "./base";
@@ -47,20 +45,20 @@ export default class SourcePoint extends AutoConsentCMPBase {
47
45
  return true;
48
46
  }
49
47
  if (this.ccpaPopup) {
50
- return await waitForElement('.priv-save-btn', 2000);
48
+ return await this.waitForElement('.priv-save-btn', 2000);
51
49
  }
52
50
  // check for the paywall button, and bail if it exists to prevent broken opt out
53
- await waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT", 2000);
54
- return !elementExists('.sp_choice_type_9');
51
+ await this.waitForElement(".sp_choice_type_11,.sp_choice_type_12,.sp_choice_type_13,.sp_choice_type_ACCEPT_ALL,.sp_choice_type_SAVE_AND_EXIT", 2000);
52
+ return !this.elementExists('.sp_choice_type_9');
55
53
  }
56
54
 
57
55
  async optIn() {
58
- await waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL", 2000);
59
- if (click(".sp_choice_type_11")) {
56
+ await this.waitForElement(".sp_choice_type_11,.sp_choice_type_ACCEPT_ALL", 2000);
57
+ if (this.click(".sp_choice_type_11")) {
60
58
  return true;
61
59
  }
62
60
 
63
- if (click('.sp_choice_type_ACCEPT_ALL')) {
61
+ if (this.click('.sp_choice_type_ACCEPT_ALL')) {
64
62
  return true;
65
63
  }
66
64
  return false;
@@ -71,6 +69,7 @@ export default class SourcePoint extends AutoConsentCMPBase {
71
69
  }
72
70
 
73
71
  async optOut() {
72
+ const logsConfig = this.autoconsent.config.logs;
74
73
  if (this.ccpaPopup) {
75
74
  // toggles with 2 buttons
76
75
  const toggles = document.querySelectorAll('.priv-purpose-container .sp-switch-arrow-block a.neutral.on .right') as NodeListOf<HTMLElement>;
@@ -82,19 +81,19 @@ export default class SourcePoint extends AutoConsentCMPBase {
82
81
  for (const t of switches) {
83
82
  t.click()
84
83
  }
85
- return click('.priv-save-btn');
84
+ return this.click('.priv-save-btn');
86
85
  }
87
86
  if (!this.isManagerOpen()) {
88
- const actionable = await waitForElement('.sp_choice_type_12,.sp_choice_type_13');
87
+ const actionable = await this.waitForElement('.sp_choice_type_12,.sp_choice_type_13');
89
88
  if (!actionable) {
90
89
  return false;
91
90
  }
92
- if (!elementExists(".sp_choice_type_12")) {
91
+ if (!this.elementExists(".sp_choice_type_12")) {
93
92
  // do not sell button
94
- return click(".sp_choice_type_13");
93
+ return this.click(".sp_choice_type_13");
95
94
  }
96
95
 
97
- click(".sp_choice_type_12");
96
+ this.click(".sp_choice_type_12");
98
97
  // the page may navigate at this point but that's okay
99
98
  await waitFor(
100
99
  () => this.isManagerOpen(),
@@ -103,35 +102,35 @@ export default class SourcePoint extends AutoConsentCMPBase {
103
102
  );
104
103
  }
105
104
 
106
- await waitForElement('.type-modal', 20000);
105
+ await this.waitForElement('.type-modal', 20000);
107
106
 
108
107
  // check "Do Not Sell" (CCPA) toggle if it exists
109
- waitForThenClick('.ccpa-stack .pm-switch[aria-checked=true] .slider', 500, true); // the UI is reversed: "unchecked" switch displays as an enabled toggle
108
+ this.waitForThenClick('.ccpa-stack .pm-switch[aria-checked=true] .slider', 500, true); // the UI is reversed: "unchecked" switch displays as an enabled toggle
110
109
 
111
110
  // reject all button is offered by some sites
112
111
  try {
113
112
  const rejectSelector1 = '.sp_choice_type_REJECT_ALL';
114
113
  const rejectSelector2 = '.reject-toggle';
115
114
  const path = await Promise.race([
116
- waitForElement(rejectSelector1, 2000).then(success => success ? 0: -1),
117
- waitForElement(rejectSelector2, 2000).then(success => success ? 1: -1),
118
- waitForElement('.pm-features', 2000).then(success => success ? 2: -1),
115
+ this.waitForElement(rejectSelector1, 2000).then(success => success ? 0: -1),
116
+ this.waitForElement(rejectSelector2, 2000).then(success => success ? 1: -1),
117
+ this.waitForElement('.pm-features', 2000).then(success => success ? 2: -1),
119
118
  ]);
120
119
  if (path === 0) {
121
- await wait(1000);
122
- return click(rejectSelector1);
120
+ await this.wait(1000);
121
+ return this.click(rejectSelector1);
123
122
  } else if (path === 1) {
124
- click(rejectSelector2);
123
+ this.click(rejectSelector2);
125
124
  } else if (path === 2) {
126
- await waitForElement('.pm-features', 10000);
127
- click('.checked > span', true);
125
+ await this.waitForElement('.pm-features', 10000);
126
+ this.click('.checked > span', true);
128
127
 
129
- click('.chevron');
128
+ this.click('.chevron');
130
129
  }
131
130
  } catch (e) {
132
- enableLogs && console.warn(e);
131
+ logsConfig.errors && console.warn(e);
133
132
  }
134
133
  // TODO: race condition: if the reject button was clicked, the popup disappears very quickly, so the background script may not receive a success report.
135
- return click('.sp_choice_type_SAVE_AND_EXIT');
134
+ return this.click('.sp_choice_type_SAVE_AND_EXIT');
136
135
  }
137
136
  }
@@ -1,5 +1,3 @@
1
- import { enableLogs } from "../config";
2
- import { elementExists } from "../rule-executors";
3
1
  import { RunContext } from "../rules";
4
2
  import { isElementVisible } from "../utils";
5
3
  import AutoConsentCMPBase from "./base";
@@ -32,7 +30,7 @@ export default class Tiktok extends AutoConsentCMPBase {
32
30
  }
33
31
 
34
32
  async detectCmp() {
35
- return elementExists("tiktok-cookie-banner");
33
+ return this.elementExists("tiktok-cookie-banner");
36
34
  }
37
35
 
38
36
  async detectPopup() {
@@ -41,25 +39,27 @@ export default class Tiktok extends AutoConsentCMPBase {
41
39
  }
42
40
 
43
41
  async optOut() {
42
+ const logsConfig = this.autoconsent.config.logs;
44
43
  const declineButton = this.getShadowRoot().querySelector('.button-wrapper button:first-child') as HTMLElement;
45
44
  if (declineButton) {
46
- enableLogs && console.log("[clicking]", declineButton);
45
+ logsConfig.rulesteps && console.log("[clicking]", declineButton);
47
46
  declineButton.click();
48
47
  return true;
49
48
  } else {
50
- enableLogs && console.log("no decline button found");
49
+ logsConfig.errors && console.log("no decline button found");
51
50
  return false;
52
51
  }
53
52
  }
54
53
 
55
54
  async optIn() {
55
+ const logsConfig = this.autoconsent.config.logs;
56
56
  const acceptButton = this.getShadowRoot().querySelector('.button-wrapper button:last-child') as HTMLElement;
57
57
  if (acceptButton) {
58
- enableLogs && console.log("[clicking]", acceptButton);
58
+ logsConfig.rulesteps && console.log("[clicking]", acceptButton);
59
59
  acceptButton.click();
60
60
  return true;
61
61
  } else {
62
- enableLogs && console.log("no accept button found");
62
+ logsConfig.errors && console.log("no accept button found");
63
63
  return false;
64
64
  }
65
65
  }
@@ -1,4 +1,3 @@
1
- import { click, elementExists, elementVisible, waitForElement } from "../rule-executors";
2
1
  import { RunContext } from "../rules";
3
2
  import { waitFor } from "../utils";
4
3
  import AutoConsentCMPBase from "./base";
@@ -30,7 +29,7 @@ export default class TrustArcFrame extends AutoConsentCMPBase {
30
29
 
31
30
  async detectPopup() {
32
31
  // we're already inside the popup
33
- return elementVisible("#defaultpreferencemanager", 'any') && elementVisible(".mainContent", 'any');
32
+ return this.elementVisible("#defaultpreferencemanager", 'any') && this.elementVisible(".mainContent", 'any');
34
33
  }
35
34
 
36
35
  async navigateToSettings() {
@@ -38,29 +37,29 @@ export default class TrustArcFrame extends AutoConsentCMPBase {
38
37
  await waitFor(
39
38
  async () => {
40
39
  return (
41
- elementExists(".shp") ||
42
- elementVisible(".advance", 'any') ||
43
- elementExists(".switch span:first-child")
40
+ this.elementExists(".shp") ||
41
+ this.elementVisible(".advance", 'any') ||
42
+ this.elementExists(".switch span:first-child")
44
43
  );
45
44
  },
46
45
  10,
47
46
  500
48
47
  );
49
48
  // splash screen -> hit more information
50
- if (elementExists(".shp")) {
51
- click(".shp");
49
+ if (this.elementExists(".shp")) {
50
+ this.click(".shp");
52
51
  }
53
52
 
54
- await waitForElement(".prefPanel", 5000);
53
+ await this.waitForElement(".prefPanel", 5000);
55
54
 
56
55
  // go to advanced settings if not yet shown
57
- if (elementVisible(".advance", 'any')) {
58
- click(".advance");
56
+ if (this.elementVisible(".advance", 'any')) {
57
+ this.click(".advance");
59
58
  }
60
59
 
61
60
  // takes a while to load the opt-in/opt-out buttons
62
61
  return await waitFor(
63
- () => elementVisible(".switch span:first-child", 'any'),
62
+ () => this.elementVisible(".switch span:first-child", 'any'),
64
63
  5,
65
64
  1000
66
65
  );
@@ -68,51 +67,51 @@ export default class TrustArcFrame extends AutoConsentCMPBase {
68
67
 
69
68
  async optOut() {
70
69
  await waitFor(() => document.readyState === 'complete', 20, 100);
71
- await waitForElement(".mainContent[aria-hidden=false]", 5000);
70
+ await this.waitForElement(".mainContent[aria-hidden=false]", 5000);
72
71
 
73
- if (click(".rejectAll")) {
72
+ if (this.click(".rejectAll")) {
74
73
  return true;
75
74
  }
76
75
 
77
- if (elementExists('.prefPanel')) {
78
- await waitForElement('.prefPanel[style="visibility: visible;"]', 3000);
76
+ if (this.elementExists('.prefPanel')) {
77
+ await this.waitForElement('.prefPanel[style="visibility: visible;"]', 3000);
79
78
  }
80
79
 
81
- if (click("#catDetails0")) {
82
- click(".submit");
83
- waitForThenClick("#gwt-debug-close_id", 5000);
80
+ if (this.click("#catDetails0")) {
81
+ this.click(".submit");
82
+ this.waitForThenClick("#gwt-debug-close_id", 5000);
84
83
  return true;
85
84
  }
86
85
 
87
- if (click(".required")) {
88
- waitForThenClick("#gwt-debug-close_id", 5000);
86
+ if (this.click(".required")) {
87
+ this.waitForThenClick("#gwt-debug-close_id", 5000);
89
88
  return true;
90
89
  }
91
90
 
92
91
  await this.navigateToSettings();
93
92
 
94
- click(".switch span:nth-child(1):not(.active)", true);
93
+ this.click(".switch span:nth-child(1):not(.active)", true);
95
94
 
96
- click(".submit");
95
+ this.click(".submit");
97
96
 
98
97
  // at this point, iframe usually closes. Sometimes we need to close manually, but we don't wait for it to report success
99
- waitForThenClick("#gwt-debug-close_id", 300000);
98
+ this.waitForThenClick("#gwt-debug-close_id", 300000);
100
99
 
101
100
  return true;
102
101
  }
103
102
 
104
103
  async optIn() {
105
- if (click('.call')) {
104
+ if (this.click('.call')) {
106
105
  return true;
107
106
  }
108
107
  await this.navigateToSettings();
109
- click(".switch span:nth-child(2)", true);
108
+ this.click(".switch span:nth-child(2)", true);
110
109
 
111
- click(".submit");
110
+ this.click(".submit");
112
111
 
113
112
  // at this point, iframe usually closes. Sometimes we need to close manually, but we don't wait for it to report success
114
- waitForElement("#gwt-debug-close_id", 300000).then(() => {
115
- click("#gwt-debug-close_id");
113
+ this.waitForElement("#gwt-debug-close_id", 300000).then(() => {
114
+ this.click("#gwt-debug-close_id");
116
115
  });
117
116
 
118
117
  return true;
@@ -1,4 +1,3 @@
1
- import { click, elementExists, elementVisible } from "../rule-executors";
2
1
  import { RunContext } from "../rules";
3
2
  import { getStyleElement, hideElements } from "../utils";
4
3
  import AutoConsent from "../web";
@@ -47,7 +46,7 @@ export default class TrustArcTop extends AutoConsentCMPBase {
47
46
  }
48
47
 
49
48
  async detectCmp() {
50
- const result = elementExists(`${cookieSettingsButton},${bannerContainer}`);
49
+ const result = this.elementExists(`${cookieSettingsButton},${bannerContainer}`);
51
50
  if (result) {
52
51
  // additionally detect the opt-out button
53
52
  this._shortcutButton = document.querySelector(shortcutOptOut);
@@ -57,11 +56,11 @@ export default class TrustArcTop extends AutoConsentCMPBase {
57
56
 
58
57
  async detectPopup() {
59
58
  // not every element should exist, but if it does, it's a popup
60
- return elementVisible(`${popupContent},${bannerOverlay},${bannerContainer}`, 'all');
59
+ return this.elementVisible(`${popupContent},${bannerOverlay},${bannerContainer}`, 'all');
61
60
  }
62
61
 
63
62
  openFrame() {
64
- click(cookieSettingsButton);
63
+ this.click(cookieSettingsButton);
65
64
  }
66
65
 
67
66
  async optOut() {
@@ -75,7 +74,7 @@ export default class TrustArcTop extends AutoConsentCMPBase {
75
74
  getStyleElement(),
76
75
  `.truste_popframe, .truste_overlay, .truste_box_overlay, ${bannerContainer}`,
77
76
  );
78
- click(cookieSettingsButton);
77
+ this.click(cookieSettingsButton);
79
78
 
80
79
  // schedule cleanup
81
80
  setTimeout(() => {
@@ -87,7 +86,7 @@ export default class TrustArcTop extends AutoConsentCMPBase {
87
86
 
88
87
  async optIn() {
89
88
  this._optInDone = true; // just a hack to force autoconsentDone
90
- return click(shortcutOptIn);
89
+ return this.click(shortcutOptIn);
91
90
  }
92
91
 
93
92
  async openCmp() {
@@ -1,4 +1,3 @@
1
- import { elementExists, elementVisible, wait, waitForElement, waitForThenClick } from "../rule-executors";
2
1
  import AutoConsentCMPBase from "./base";
3
2
 
4
3
  export default class Uniconsent extends AutoConsentCMPBase {
@@ -21,15 +20,15 @@ export default class Uniconsent extends AutoConsentCMPBase {
21
20
  }
22
21
 
23
22
  async detectCmp() {
24
- return elementExists(".unic .unic-box,.unic .unic-bar");
23
+ return this.elementExists(".unic .unic-box,.unic .unic-bar");
25
24
  }
26
25
 
27
26
  async detectPopup() {
28
- return elementVisible(".unic .unic-box,.unic .unic-bar", 'any');
27
+ return this.elementVisible(".unic .unic-box,.unic .unic-bar", 'any');
29
28
  }
30
29
 
31
30
  async optOut() {
32
- await waitForElement(".unic button", 1000);
31
+ await this.waitForElement(".unic button", 1000);
33
32
  document.querySelectorAll(".unic button").forEach((button: HTMLButtonElement) => {
34
33
  const text = button.textContent;
35
34
  if (text.includes('Manage Options') || text.includes('Optionen verwalten')) {
@@ -37,8 +36,8 @@ export default class Uniconsent extends AutoConsentCMPBase {
37
36
  }
38
37
  });
39
38
 
40
- if (await waitForElement('.unic input[type=checkbox]', 1000)) {
41
- await waitForElement('.unic button', 1000);
39
+ if (await this.waitForElement('.unic input[type=checkbox]', 1000)) {
40
+ await this.waitForElement('.unic button', 1000);
42
41
 
43
42
  document.querySelectorAll('.unic input[type=checkbox]').forEach((c: HTMLInputElement) => {
44
43
  if (c.checked) {
@@ -51,7 +50,7 @@ export default class Uniconsent extends AutoConsentCMPBase {
51
50
  for (const pattern of ['Confirm Choices', 'Save Choices', 'Auswahl speichern']) {
52
51
  if (text.includes(pattern)) {
53
52
  b.click();
54
- await wait(500); // give it some time to close the popup
53
+ await this.wait(500); // give it some time to close the popup
55
54
  return true;
56
55
  }
57
56
  }
@@ -62,12 +61,12 @@ export default class Uniconsent extends AutoConsentCMPBase {
62
61
  }
63
62
 
64
63
  async optIn() {
65
- return waitForThenClick(".unic #unic-agree");
64
+ return this.waitForThenClick(".unic #unic-agree");
66
65
  }
67
66
 
68
67
  async test() {
69
- await wait(1000);
70
- const res = elementExists(".unic .unic-box,.unic .unic-bar");
68
+ await this.wait(1000);
69
+ const res = this.elementExists(".unic .unic-box,.unic .unic-bar");
71
70
  return !res;
72
71
  }
73
72
  }
@@ -0,0 +1,145 @@
1
+ import { ElementSelector, HideMethod, VisibilityCheck } from "./rules";
2
+ import { DomActionsProvider } from "./types";
3
+ import { getStyleElement, hideElements, isElementVisible, waitFor } from "./utils";
4
+ import AutoConsent from "./web";
5
+
6
+ export class DomActions implements DomActionsProvider {
7
+ constructor(public autoconsentInstance: AutoConsent) { }
8
+
9
+ click(selector: ElementSelector, all = false): boolean {
10
+ const elem = this.elementSelector(selector)
11
+ this.autoconsentInstance.config.logs.rulesteps && console.log("[click]", selector, all, elem);
12
+ if (elem.length > 0) {
13
+ if (all) {
14
+ elem.forEach((e) => e.click());
15
+ } else {
16
+ elem[0].click();
17
+ }
18
+ }
19
+ return elem.length > 0;
20
+ }
21
+
22
+ elementExists(selector: ElementSelector): boolean {
23
+ const exists = this.elementSelector(selector).length > 0;
24
+ return exists;
25
+ }
26
+
27
+ elementVisible(selector: ElementSelector, check: VisibilityCheck): boolean {
28
+ const elem = this.elementSelector(selector);
29
+ const results = new Array(elem.length);
30
+ elem.forEach((e, i) => {
31
+ // check for display: none
32
+ results[i] = isElementVisible(e);
33
+ });
34
+ if (check === "none") {
35
+ return results.every(r => !r);
36
+ } else if (results.length === 0) {
37
+ return false;
38
+ } else if (check === "any") {
39
+ return results.some(r => r);
40
+ }
41
+ // all
42
+ return results.every(r => r);
43
+ }
44
+
45
+ waitForElement(selector: ElementSelector, timeout = 10000): Promise<boolean> {
46
+ const interval = 200;
47
+ const times = Math.ceil((timeout) / interval);
48
+ this.autoconsentInstance.config.logs.rulesteps && console.log("[waitForElement]", selector);
49
+ return waitFor(
50
+ () => this.elementSelector(selector).length > 0,
51
+ times,
52
+ interval
53
+ );
54
+ }
55
+
56
+ waitForVisible(selector: ElementSelector, timeout = 10000, check: VisibilityCheck = 'any'): Promise<boolean> {
57
+ const interval = 200;
58
+ const times = Math.ceil((timeout) / interval);
59
+ return waitFor(
60
+ () => this.elementVisible(selector, check),
61
+ times,
62
+ interval
63
+ );
64
+ }
65
+
66
+ async waitForThenClick(selector: ElementSelector, timeout = 10000, all = false): Promise<boolean> {
67
+ await this.waitForElement(selector, timeout);
68
+ return this.click(selector, all);
69
+ }
70
+
71
+ wait(ms: number): Promise<true> {
72
+ return new Promise(resolve => {
73
+ setTimeout(() => {
74
+ resolve(true);
75
+ }, ms);
76
+ });
77
+ }
78
+
79
+ hide(selector: string, method: HideMethod): boolean {
80
+ const styleEl = getStyleElement();
81
+ return hideElements(styleEl, selector, method);
82
+ }
83
+
84
+ prehide(selector: string): boolean {
85
+ const styleEl = getStyleElement('autoconsent-prehide');
86
+ this.autoconsentInstance.config.logs.lifecycle && console.log("[prehide]", styleEl, location.href);
87
+ return hideElements(styleEl, selector, "opacity");
88
+ }
89
+
90
+ undoPrehide(): boolean {
91
+ const existingElement = getStyleElement('autoconsent-prehide');
92
+ this.autoconsentInstance.config.logs.lifecycle && console.log("[undoprehide]", existingElement, location.href);
93
+ if (existingElement) {
94
+ existingElement.remove();
95
+ }
96
+ return !!existingElement;
97
+ }
98
+
99
+ querySingleReplySelector(selector: string, parent: any = document): HTMLElement[] {
100
+ if (selector.startsWith('aria/')) {
101
+ return []
102
+ }
103
+ if (selector.startsWith('xpath/')) {
104
+ const xpath = selector.slice(6)
105
+ const result = document.evaluate(xpath, parent, null, XPathResult.ANY_TYPE, null)
106
+ let node: Node = null
107
+ const elements: HTMLElement[] = []
108
+ // eslint-disable-next-line no-cond-assign
109
+ while (node = result.iterateNext()) {
110
+ elements.push(node as HTMLElement)
111
+ }
112
+ return elements
113
+ }
114
+ if (selector.startsWith('text/')) {
115
+ return []
116
+ }
117
+ if (selector.startsWith('pierce/')) {
118
+ return []
119
+ }
120
+ if (parent.shadowRoot) {
121
+ return Array.from(parent.shadowRoot.querySelectorAll(selector))
122
+ }
123
+ return Array.from(parent.querySelectorAll(selector))
124
+ }
125
+
126
+ querySelectorChain(selectors: string[]): HTMLElement[] {
127
+ let parent: ParentNode = document
128
+ let matches: HTMLElement[]
129
+ for (const selector of selectors) {
130
+ matches = this.querySingleReplySelector(selector, parent)
131
+ if (matches.length === 0) {
132
+ return []
133
+ }
134
+ parent = matches[0]
135
+ }
136
+ return matches;
137
+ }
138
+
139
+ elementSelector(selector: ElementSelector): HTMLElement[] {
140
+ if (typeof selector === 'string') {
141
+ return this.querySingleReplySelector(selector)
142
+ }
143
+ return this.querySelectorChain(selector)
144
+ }
145
+ }
package/lib/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ContentScriptMessage } from "./messages";
2
2
  import { ConsentOMaticConfig } from "./cmps/consentomatic";
3
- import { AutoConsentCMPRule, RunContext } from "./rules";
3
+ import { AutoConsentCMPRule, ElementSelector, HideMethod, RunContext, VisibilityCheck } from "./rules";
4
4
 
5
5
  export type MessageSender = (message: ContentScriptMessage) => Promise<void>;
6
6
 
@@ -20,6 +20,22 @@ export interface AutoCMP {
20
20
  test(): Promise<boolean>
21
21
  }
22
22
 
23
+ export interface DomActionsProvider {
24
+ click(selector: ElementSelector, all: boolean): boolean;
25
+ elementExists(selector: ElementSelector): boolean;
26
+ elementVisible(selector: ElementSelector, check: VisibilityCheck): boolean;
27
+ waitForElement(selector: ElementSelector, timeout?: number): Promise<boolean>;
28
+ waitForVisible(selector: ElementSelector, timeout?: number, check?: VisibilityCheck): Promise<boolean>;
29
+ waitForThenClick(selector: ElementSelector, timeout?: number, all?: boolean): Promise<boolean>;
30
+ wait(ms: number): Promise<true>;
31
+ hide(selector: string, method: HideMethod): boolean;
32
+ prehide(selector: string): boolean;
33
+ undoPrehide(): boolean;
34
+ querySingleReplySelector(selector: string, parent?: any): HTMLElement[];
35
+ querySelectorChain(selectors: string[]): HTMLElement[];
36
+ elementSelector(selector: ElementSelector): HTMLElement[];
37
+ }
38
+
23
39
  export type RuleBundle = {
24
40
  autoconsent: AutoConsentCMPRule[];
25
41
  consentomatic: { [name: string]: ConsentOMaticConfig };
@@ -36,6 +52,13 @@ export type Config = {
36
52
  detectRetries: number;
37
53
  isMainWorld: boolean;
38
54
  prehideTimeout: number;
55
+ logs: {
56
+ lifecycle: boolean;
57
+ rulesteps: boolean;
58
+ evals: boolean;
59
+ errors: boolean;
60
+ messages: boolean;
61
+ };
39
62
  }
40
63
 
41
64
  export type LifecycleState = 'loading' |
package/lib/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HideMethod } from "./rules";
2
+ import { Config } from "./types";
2
3
 
3
4
  // get or create a style container for CSS overrides
4
5
  export function getStyleElement(styleOverrideElementId = "autoconsent-css-rules"): HTMLStyleElement {
@@ -59,4 +60,34 @@ export function isElementVisible(elem: HTMLElement): boolean {
59
60
  }
60
61
  }
61
62
  return false;
62
- }
63
+ }
64
+
65
+ export function normalizeConfig(providedConfig: any): Config {
66
+ const defaultConfig: Config = {
67
+ enabled: true,
68
+ autoAction: 'optOut', // if falsy, the extension will wait for an explicit user signal before opting in/out
69
+ disabledCmps: [],
70
+ enablePrehide: true,
71
+ enableCosmeticRules: true,
72
+ detectRetries: 20,
73
+ isMainWorld: false,
74
+ prehideTimeout: 2000,
75
+ logs: {
76
+ lifecycle: false,
77
+ rulesteps: false,
78
+ evals: false,
79
+ errors: true,
80
+ messages: false,
81
+ },
82
+ };
83
+ const updatedConfig: Config = structuredClone(defaultConfig);
84
+ // filter out any unknown entries
85
+ for (const key of Object.keys(defaultConfig)) {
86
+ if (typeof providedConfig[key] !== 'undefined') {
87
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
+ // @ts-expect-error - TS doesn't know that we've checked for undefined
89
+ updatedConfig[key] = providedConfig[key];
90
+ }
91
+ }
92
+ return updatedConfig;
93
+ }