@duckduckgo/autoconsent 14.82.0 → 14.83.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.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +14 -0
- package/dist/addon-firefox/background.bundle.js +7 -0
- package/dist/addon-firefox/content.bundle.js +16 -2
- package/dist/addon-firefox/manifest.json +1 -1
- package/dist/addon-firefox/popup.bundle.js +20 -0
- package/dist/addon-firefox/popup.html +14 -0
- package/dist/addon-mv3/background.bundle.js +7 -0
- package/dist/addon-mv3/content.bundle.js +16 -2
- package/dist/addon-mv3/manifest.json +1 -1
- package/dist/addon-mv3/popup.bundle.js +20 -0
- package/dist/addon-mv3/popup.html +14 -0
- package/dist/autoconsent.cjs.js +16 -2
- package/dist/autoconsent.esm.js +16 -2
- package/dist/autoconsent.extra.cjs.js +16 -2
- package/dist/autoconsent.extra.esm.js +16 -2
- package/dist/autoconsent.playwright.js +16 -2
- package/dist/types/types.d.ts +1 -0
- package/lib/types.ts +1 -0
- package/lib/utils.ts +1 -0
- package/lib/web.ts +17 -3
- package/package.json +1 -1
- package/tests-wtr/lifecycle/wait-for-popup.html +14 -0
- package/tests-wtr/lifecycle/wait-for-popup.ts +214 -0
package/AGENTS.md
CHANGED
|
@@ -107,6 +107,7 @@ shadow root or same-origin iframe.
|
|
|
107
107
|
- **Paywalls do not need to be handled.** If the website presents the choice to pay or agree to cookies, the correct solution is to disable the feature on that site, so no code changes required in this case.
|
|
108
108
|
- If the pop-up has an explicit "reject"-like button, you should first consider why HEURISTIC rule didn't handle it. A fix to the heuristic rule is always preferred to a new rule, as long as it doesn't cause potential false-positives on other sites.
|
|
109
109
|
- **Watch out for race conditions**. A common pitfall is that a rule starts clicking before JS handlers are ready. If you detect this, add an appropriate wait step before the click, preferably based on a specific DOM state. Unconditional `wait` is a LAST RESORT because it leads to a poor UX.
|
|
110
|
+
- **Watch out for false positive detections**. Always verify that the rule does NOT match after the popup is dismissed and the page is reloaded. Over-detection can lead to reload loops.
|
|
110
111
|
- **selfTests are optional.** It is okay to NOT have a self-test, or have it failing as long as the popup is handled correctly. Confirm this with screenshots.
|
|
111
112
|
- If you cover a new CMP or a new flavor of the existing CMP, ALWAYS try to look for more examples of that case, and add to the spec file.
|
|
112
113
|
- `detectCmp` and `detectPopup` must be fast. Do NOT use waiting steps — the engine retries automatically.
|
|
@@ -157,4 +158,5 @@ After creating or modifying a rule:
|
|
|
157
158
|
2. `npm run rule-syntax-check` — validate rule JSON against schema
|
|
158
159
|
3. `npx playwright test tests/<name>.spec.ts` — run the E2E test
|
|
159
160
|
4. `npm run prepublish` — full build including extension bundle
|
|
160
|
-
5.
|
|
161
|
+
5. Validate that the rule stops matching after the popup is dismissed and the page is reloaded (unless it's a cosmetic rule).
|
|
162
|
+
6. Check the rule works across geographic regions using available regional-testing tooling.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# v14.83.0 (Sat May 16 2026)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- Add a hint about reload loops [#1352](https://github.com/duckduckgo/autoconsent/pull/1352) ([@muodov](https://github.com/muodov))
|
|
6
|
+
- Use MutationObserver in waitForPopup to avoid premature timeout [#1207](https://github.com/duckduckgo/autoconsent/pull/1207) ([@cursoragent](https://github.com/cursoragent) [@muodov](https://github.com/muodov))
|
|
7
|
+
|
|
8
|
+
#### Authors: 2
|
|
9
|
+
|
|
10
|
+
- Cursor Agent ([@cursoragent](https://github.com/cursoragent))
|
|
11
|
+
- Maxim Tsoy ([@muodov](https://github.com/muodov))
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
1
15
|
# v14.82.0 (Fri May 15 2026)
|
|
2
16
|
|
|
3
17
|
#### 🚀 Enhancement
|
|
@@ -697,6 +697,7 @@
|
|
|
697
697
|
enableGeneratedRules: true,
|
|
698
698
|
enableHeuristicDetection: false,
|
|
699
699
|
enableHeuristicAction: false,
|
|
700
|
+
enablePopupMutationObserver: false,
|
|
700
701
|
detectRetries: 20,
|
|
701
702
|
isMainWorld: false,
|
|
702
703
|
prehideTimeout: 2e3,
|
|
@@ -726,6 +727,12 @@
|
|
|
726
727
|
if (!storedConfig.enableHeuristicDetection) {
|
|
727
728
|
storedConfig.enableHeuristicDetection = true;
|
|
728
729
|
}
|
|
730
|
+
if (storedConfig.enablePopupMutationObserver === void 0) {
|
|
731
|
+
storedConfig.enablePopupMutationObserver = true;
|
|
732
|
+
}
|
|
733
|
+
if (storedConfig.enableHeuristicAction === void 0) {
|
|
734
|
+
storedConfig.enableHeuristicAction = true;
|
|
735
|
+
}
|
|
729
736
|
if (!storedConfig.logs) {
|
|
730
737
|
storedConfig.logs = {
|
|
731
738
|
lifecycle: true,
|
|
@@ -693,6 +693,7 @@
|
|
|
693
693
|
enableGeneratedRules: true,
|
|
694
694
|
enableHeuristicDetection: false,
|
|
695
695
|
enableHeuristicAction: false,
|
|
696
|
+
enablePopupMutationObserver: false,
|
|
696
697
|
detectRetries: 20,
|
|
697
698
|
isMainWorld: false,
|
|
698
699
|
prehideTimeout: 2e3,
|
|
@@ -3695,16 +3696,29 @@
|
|
|
3695
3696
|
this.updateState({ selfTest: selfTestResult });
|
|
3696
3697
|
return selfTestResult;
|
|
3697
3698
|
}
|
|
3698
|
-
// TODO: use MutationObserver like in findCmp()
|
|
3699
3699
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
3700
3700
|
const logsConfig = this.config.logs;
|
|
3701
3701
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
3702
|
+
let mutationObserver = null;
|
|
3703
|
+
if (this.config.enablePopupMutationObserver) {
|
|
3704
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
3705
|
+
mutationObserver.catch(() => {
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3702
3708
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
3703
3709
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
3704
3710
|
return false;
|
|
3705
3711
|
});
|
|
3706
3712
|
if (!isOpen && retries > 0) {
|
|
3707
|
-
|
|
3713
|
+
if (mutationObserver) {
|
|
3714
|
+
try {
|
|
3715
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
3716
|
+
} catch (e) {
|
|
3717
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
3718
|
+
}
|
|
3719
|
+
} else {
|
|
3720
|
+
await this.domActions.wait(interval);
|
|
3721
|
+
}
|
|
3708
3722
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
3709
3723
|
}
|
|
3710
3724
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
|
@@ -524,6 +524,7 @@
|
|
|
524
524
|
enableGeneratedRules: true,
|
|
525
525
|
enableHeuristicDetection: false,
|
|
526
526
|
enableHeuristicAction: false,
|
|
527
|
+
enablePopupMutationObserver: false,
|
|
527
528
|
detectRetries: 20,
|
|
528
529
|
isMainWorld: false,
|
|
529
530
|
prehideTimeout: 2e3,
|
|
@@ -553,6 +554,12 @@
|
|
|
553
554
|
if (!storedConfig.enableHeuristicDetection) {
|
|
554
555
|
storedConfig.enableHeuristicDetection = true;
|
|
555
556
|
}
|
|
557
|
+
if (storedConfig.enablePopupMutationObserver === void 0) {
|
|
558
|
+
storedConfig.enablePopupMutationObserver = true;
|
|
559
|
+
}
|
|
560
|
+
if (storedConfig.enableHeuristicAction === void 0) {
|
|
561
|
+
storedConfig.enableHeuristicAction = true;
|
|
562
|
+
}
|
|
556
563
|
if (!storedConfig.logs) {
|
|
557
564
|
storedConfig.logs = {
|
|
558
565
|
lifecycle: true,
|
|
@@ -654,6 +661,8 @@
|
|
|
654
661
|
const heuristicActionOffRadio = document.querySelector("input#heuristic-action-off");
|
|
655
662
|
const visualTestOnRadio = document.querySelector("input#visual-test-on");
|
|
656
663
|
const visualTestOffRadio = document.querySelector("input#visual-test-off");
|
|
664
|
+
const popupMutationOnRadio = document.querySelector("input#popup-mutation-on");
|
|
665
|
+
const popupMutationOffRadio = document.querySelector("input#popup-mutation-off");
|
|
657
666
|
const retriesInput = document.querySelector("input#retries");
|
|
658
667
|
const logsLifecycleCheckbox = document.querySelector("input#logs-lifecycle");
|
|
659
668
|
const logsRulestepsCheckbox = document.querySelector("input#logs-rulesteps");
|
|
@@ -739,6 +748,11 @@
|
|
|
739
748
|
} else {
|
|
740
749
|
visualTestOffRadio.checked = true;
|
|
741
750
|
}
|
|
751
|
+
if (autoconsentConfig.enablePopupMutationObserver) {
|
|
752
|
+
popupMutationOnRadio.checked = true;
|
|
753
|
+
} else {
|
|
754
|
+
popupMutationOffRadio.checked = true;
|
|
755
|
+
}
|
|
742
756
|
enabledCheckbox.addEventListener("change", async () => {
|
|
743
757
|
await setIsEnabledForDomain(currentDomain, enabledCheckbox.checked);
|
|
744
758
|
});
|
|
@@ -789,6 +803,12 @@
|
|
|
789
803
|
}
|
|
790
804
|
visualTestOnRadio.addEventListener("change", visualTestChange);
|
|
791
805
|
visualTestOffRadio.addEventListener("change", visualTestChange);
|
|
806
|
+
function popupMutationChange() {
|
|
807
|
+
autoconsentConfig.enablePopupMutationObserver = popupMutationOnRadio.checked;
|
|
808
|
+
storageSet({ config: autoconsentConfig });
|
|
809
|
+
}
|
|
810
|
+
popupMutationOnRadio.addEventListener("change", popupMutationChange);
|
|
811
|
+
popupMutationOffRadio.addEventListener("change", popupMutationChange);
|
|
792
812
|
function updateLogsConfig() {
|
|
793
813
|
autoconsentConfig.logs = {
|
|
794
814
|
lifecycle: logsLifecycleCheckbox.checked,
|
|
@@ -123,6 +123,20 @@
|
|
|
123
123
|
</div>
|
|
124
124
|
</fieldset>
|
|
125
125
|
|
|
126
|
+
<fieldset>
|
|
127
|
+
<legend>Popup mutation observer</legend>
|
|
128
|
+
|
|
129
|
+
<div>
|
|
130
|
+
<input type="radio" id="popup-mutation-on" name="popup-mutation" value="true" />
|
|
131
|
+
<label for="popup-mutation-on">On</label>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
<input type="radio" id="popup-mutation-off" name="popup-mutation" value="false" checked />
|
|
136
|
+
<label for="popup-mutation-off">Off</label>
|
|
137
|
+
</div>
|
|
138
|
+
</fieldset>
|
|
139
|
+
|
|
126
140
|
<fieldset>
|
|
127
141
|
<legend>Debug logging</legend>
|
|
128
142
|
<div>
|
|
@@ -697,6 +697,7 @@
|
|
|
697
697
|
enableGeneratedRules: true,
|
|
698
698
|
enableHeuristicDetection: false,
|
|
699
699
|
enableHeuristicAction: false,
|
|
700
|
+
enablePopupMutationObserver: false,
|
|
700
701
|
detectRetries: 20,
|
|
701
702
|
isMainWorld: false,
|
|
702
703
|
prehideTimeout: 2e3,
|
|
@@ -726,6 +727,12 @@
|
|
|
726
727
|
if (!storedConfig.enableHeuristicDetection) {
|
|
727
728
|
storedConfig.enableHeuristicDetection = true;
|
|
728
729
|
}
|
|
730
|
+
if (storedConfig.enablePopupMutationObserver === void 0) {
|
|
731
|
+
storedConfig.enablePopupMutationObserver = true;
|
|
732
|
+
}
|
|
733
|
+
if (storedConfig.enableHeuristicAction === void 0) {
|
|
734
|
+
storedConfig.enableHeuristicAction = true;
|
|
735
|
+
}
|
|
729
736
|
if (!storedConfig.logs) {
|
|
730
737
|
storedConfig.logs = {
|
|
731
738
|
lifecycle: true,
|
|
@@ -693,6 +693,7 @@
|
|
|
693
693
|
enableGeneratedRules: true,
|
|
694
694
|
enableHeuristicDetection: false,
|
|
695
695
|
enableHeuristicAction: false,
|
|
696
|
+
enablePopupMutationObserver: false,
|
|
696
697
|
detectRetries: 20,
|
|
697
698
|
isMainWorld: false,
|
|
698
699
|
prehideTimeout: 2e3,
|
|
@@ -3695,16 +3696,29 @@
|
|
|
3695
3696
|
this.updateState({ selfTest: selfTestResult });
|
|
3696
3697
|
return selfTestResult;
|
|
3697
3698
|
}
|
|
3698
|
-
// TODO: use MutationObserver like in findCmp()
|
|
3699
3699
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
3700
3700
|
const logsConfig = this.config.logs;
|
|
3701
3701
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
3702
|
+
let mutationObserver = null;
|
|
3703
|
+
if (this.config.enablePopupMutationObserver) {
|
|
3704
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
3705
|
+
mutationObserver.catch(() => {
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3702
3708
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
3703
3709
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
3704
3710
|
return false;
|
|
3705
3711
|
});
|
|
3706
3712
|
if (!isOpen && retries > 0) {
|
|
3707
|
-
|
|
3713
|
+
if (mutationObserver) {
|
|
3714
|
+
try {
|
|
3715
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
3716
|
+
} catch (e) {
|
|
3717
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
3718
|
+
}
|
|
3719
|
+
} else {
|
|
3720
|
+
await this.domActions.wait(interval);
|
|
3721
|
+
}
|
|
3708
3722
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
3709
3723
|
}
|
|
3710
3724
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
|
@@ -524,6 +524,7 @@
|
|
|
524
524
|
enableGeneratedRules: true,
|
|
525
525
|
enableHeuristicDetection: false,
|
|
526
526
|
enableHeuristicAction: false,
|
|
527
|
+
enablePopupMutationObserver: false,
|
|
527
528
|
detectRetries: 20,
|
|
528
529
|
isMainWorld: false,
|
|
529
530
|
prehideTimeout: 2e3,
|
|
@@ -553,6 +554,12 @@
|
|
|
553
554
|
if (!storedConfig.enableHeuristicDetection) {
|
|
554
555
|
storedConfig.enableHeuristicDetection = true;
|
|
555
556
|
}
|
|
557
|
+
if (storedConfig.enablePopupMutationObserver === void 0) {
|
|
558
|
+
storedConfig.enablePopupMutationObserver = true;
|
|
559
|
+
}
|
|
560
|
+
if (storedConfig.enableHeuristicAction === void 0) {
|
|
561
|
+
storedConfig.enableHeuristicAction = true;
|
|
562
|
+
}
|
|
556
563
|
if (!storedConfig.logs) {
|
|
557
564
|
storedConfig.logs = {
|
|
558
565
|
lifecycle: true,
|
|
@@ -654,6 +661,8 @@
|
|
|
654
661
|
const heuristicActionOffRadio = document.querySelector("input#heuristic-action-off");
|
|
655
662
|
const visualTestOnRadio = document.querySelector("input#visual-test-on");
|
|
656
663
|
const visualTestOffRadio = document.querySelector("input#visual-test-off");
|
|
664
|
+
const popupMutationOnRadio = document.querySelector("input#popup-mutation-on");
|
|
665
|
+
const popupMutationOffRadio = document.querySelector("input#popup-mutation-off");
|
|
657
666
|
const retriesInput = document.querySelector("input#retries");
|
|
658
667
|
const logsLifecycleCheckbox = document.querySelector("input#logs-lifecycle");
|
|
659
668
|
const logsRulestepsCheckbox = document.querySelector("input#logs-rulesteps");
|
|
@@ -739,6 +748,11 @@
|
|
|
739
748
|
} else {
|
|
740
749
|
visualTestOffRadio.checked = true;
|
|
741
750
|
}
|
|
751
|
+
if (autoconsentConfig.enablePopupMutationObserver) {
|
|
752
|
+
popupMutationOnRadio.checked = true;
|
|
753
|
+
} else {
|
|
754
|
+
popupMutationOffRadio.checked = true;
|
|
755
|
+
}
|
|
742
756
|
enabledCheckbox.addEventListener("change", async () => {
|
|
743
757
|
await setIsEnabledForDomain(currentDomain, enabledCheckbox.checked);
|
|
744
758
|
});
|
|
@@ -789,6 +803,12 @@
|
|
|
789
803
|
}
|
|
790
804
|
visualTestOnRadio.addEventListener("change", visualTestChange);
|
|
791
805
|
visualTestOffRadio.addEventListener("change", visualTestChange);
|
|
806
|
+
function popupMutationChange() {
|
|
807
|
+
autoconsentConfig.enablePopupMutationObserver = popupMutationOnRadio.checked;
|
|
808
|
+
storageSet({ config: autoconsentConfig });
|
|
809
|
+
}
|
|
810
|
+
popupMutationOnRadio.addEventListener("change", popupMutationChange);
|
|
811
|
+
popupMutationOffRadio.addEventListener("change", popupMutationChange);
|
|
792
812
|
function updateLogsConfig() {
|
|
793
813
|
autoconsentConfig.logs = {
|
|
794
814
|
lifecycle: logsLifecycleCheckbox.checked,
|
|
@@ -123,6 +123,20 @@
|
|
|
123
123
|
</div>
|
|
124
124
|
</fieldset>
|
|
125
125
|
|
|
126
|
+
<fieldset>
|
|
127
|
+
<legend>Popup mutation observer</legend>
|
|
128
|
+
|
|
129
|
+
<div>
|
|
130
|
+
<input type="radio" id="popup-mutation-on" name="popup-mutation" value="true" />
|
|
131
|
+
<label for="popup-mutation-on">On</label>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
<input type="radio" id="popup-mutation-off" name="popup-mutation" value="false" checked />
|
|
136
|
+
<label for="popup-mutation-off">Off</label>
|
|
137
|
+
</div>
|
|
138
|
+
</fieldset>
|
|
139
|
+
|
|
126
140
|
<fieldset>
|
|
127
141
|
<legend>Debug logging</legend>
|
|
128
142
|
<div>
|
package/dist/autoconsent.cjs.js
CHANGED
|
@@ -728,6 +728,7 @@ function normalizeConfig(providedConfig) {
|
|
|
728
728
|
enableGeneratedRules: true,
|
|
729
729
|
enableHeuristicDetection: false,
|
|
730
730
|
enableHeuristicAction: false,
|
|
731
|
+
enablePopupMutationObserver: false,
|
|
731
732
|
detectRetries: 20,
|
|
732
733
|
isMainWorld: false,
|
|
733
734
|
prehideTimeout: 2e3,
|
|
@@ -3963,16 +3964,29 @@ var AutoConsent = class {
|
|
|
3963
3964
|
this.updateState({ selfTest: selfTestResult });
|
|
3964
3965
|
return selfTestResult;
|
|
3965
3966
|
}
|
|
3966
|
-
// TODO: use MutationObserver like in findCmp()
|
|
3967
3967
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
3968
3968
|
const logsConfig = this.config.logs;
|
|
3969
3969
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
3970
|
+
let mutationObserver = null;
|
|
3971
|
+
if (this.config.enablePopupMutationObserver) {
|
|
3972
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
3973
|
+
mutationObserver.catch(() => {
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3970
3976
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
3971
3977
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
3972
3978
|
return false;
|
|
3973
3979
|
});
|
|
3974
3980
|
if (!isOpen && retries > 0) {
|
|
3975
|
-
|
|
3981
|
+
if (mutationObserver) {
|
|
3982
|
+
try {
|
|
3983
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
3984
|
+
} catch (e) {
|
|
3985
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
3986
|
+
}
|
|
3987
|
+
} else {
|
|
3988
|
+
await this.domActions.wait(interval);
|
|
3989
|
+
}
|
|
3976
3990
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
3977
3991
|
}
|
|
3978
3992
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
package/dist/autoconsent.esm.js
CHANGED
|
@@ -698,6 +698,7 @@ function normalizeConfig(providedConfig) {
|
|
|
698
698
|
enableGeneratedRules: true,
|
|
699
699
|
enableHeuristicDetection: false,
|
|
700
700
|
enableHeuristicAction: false,
|
|
701
|
+
enablePopupMutationObserver: false,
|
|
701
702
|
detectRetries: 20,
|
|
702
703
|
isMainWorld: false,
|
|
703
704
|
prehideTimeout: 2e3,
|
|
@@ -3933,16 +3934,29 @@ var AutoConsent = class {
|
|
|
3933
3934
|
this.updateState({ selfTest: selfTestResult });
|
|
3934
3935
|
return selfTestResult;
|
|
3935
3936
|
}
|
|
3936
|
-
// TODO: use MutationObserver like in findCmp()
|
|
3937
3937
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
3938
3938
|
const logsConfig = this.config.logs;
|
|
3939
3939
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
3940
|
+
let mutationObserver = null;
|
|
3941
|
+
if (this.config.enablePopupMutationObserver) {
|
|
3942
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
3943
|
+
mutationObserver.catch(() => {
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3940
3946
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
3941
3947
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
3942
3948
|
return false;
|
|
3943
3949
|
});
|
|
3944
3950
|
if (!isOpen && retries > 0) {
|
|
3945
|
-
|
|
3951
|
+
if (mutationObserver) {
|
|
3952
|
+
try {
|
|
3953
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
3954
|
+
} catch (e) {
|
|
3955
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
3956
|
+
}
|
|
3957
|
+
} else {
|
|
3958
|
+
await this.domActions.wait(interval);
|
|
3959
|
+
}
|
|
3946
3960
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
3947
3961
|
}
|
|
3948
3962
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
|
@@ -11291,6 +11291,7 @@ function normalizeConfig(providedConfig) {
|
|
|
11291
11291
|
enableGeneratedRules: true,
|
|
11292
11292
|
enableHeuristicDetection: false,
|
|
11293
11293
|
enableHeuristicAction: false,
|
|
11294
|
+
enablePopupMutationObserver: false,
|
|
11294
11295
|
detectRetries: 20,
|
|
11295
11296
|
isMainWorld: false,
|
|
11296
11297
|
prehideTimeout: 2e3,
|
|
@@ -14961,16 +14962,29 @@ var AutoConsent = class {
|
|
|
14961
14962
|
this.updateState({ selfTest: selfTestResult });
|
|
14962
14963
|
return selfTestResult;
|
|
14963
14964
|
}
|
|
14964
|
-
// TODO: use MutationObserver like in findCmp()
|
|
14965
14965
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
14966
14966
|
const logsConfig = this.config.logs;
|
|
14967
14967
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
14968
|
+
let mutationObserver = null;
|
|
14969
|
+
if (this.config.enablePopupMutationObserver) {
|
|
14970
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
14971
|
+
mutationObserver.catch(() => {
|
|
14972
|
+
});
|
|
14973
|
+
}
|
|
14968
14974
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
14969
14975
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
14970
14976
|
return false;
|
|
14971
14977
|
});
|
|
14972
14978
|
if (!isOpen && retries > 0) {
|
|
14973
|
-
|
|
14979
|
+
if (mutationObserver) {
|
|
14980
|
+
try {
|
|
14981
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
14982
|
+
} catch (e) {
|
|
14983
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
14984
|
+
}
|
|
14985
|
+
} else {
|
|
14986
|
+
await this.domActions.wait(interval);
|
|
14987
|
+
}
|
|
14974
14988
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
14975
14989
|
}
|
|
14976
14990
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
|
@@ -11225,6 +11225,7 @@ function normalizeConfig(providedConfig) {
|
|
|
11225
11225
|
enableGeneratedRules: true,
|
|
11226
11226
|
enableHeuristicDetection: false,
|
|
11227
11227
|
enableHeuristicAction: false,
|
|
11228
|
+
enablePopupMutationObserver: false,
|
|
11228
11229
|
detectRetries: 20,
|
|
11229
11230
|
isMainWorld: false,
|
|
11230
11231
|
prehideTimeout: 2e3,
|
|
@@ -14895,16 +14896,29 @@ var AutoConsent = class {
|
|
|
14895
14896
|
this.updateState({ selfTest: selfTestResult });
|
|
14896
14897
|
return selfTestResult;
|
|
14897
14898
|
}
|
|
14898
|
-
// TODO: use MutationObserver like in findCmp()
|
|
14899
14899
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
14900
14900
|
const logsConfig = this.config.logs;
|
|
14901
14901
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
14902
|
+
let mutationObserver = null;
|
|
14903
|
+
if (this.config.enablePopupMutationObserver) {
|
|
14904
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
14905
|
+
mutationObserver.catch(() => {
|
|
14906
|
+
});
|
|
14907
|
+
}
|
|
14902
14908
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
14903
14909
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
14904
14910
|
return false;
|
|
14905
14911
|
});
|
|
14906
14912
|
if (!isOpen && retries > 0) {
|
|
14907
|
-
|
|
14913
|
+
if (mutationObserver) {
|
|
14914
|
+
try {
|
|
14915
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
14916
|
+
} catch (e) {
|
|
14917
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
14918
|
+
}
|
|
14919
|
+
} else {
|
|
14920
|
+
await this.domActions.wait(interval);
|
|
14921
|
+
}
|
|
14908
14922
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
14909
14923
|
}
|
|
14910
14924
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
|
@@ -700,6 +700,7 @@
|
|
|
700
700
|
enableGeneratedRules: true,
|
|
701
701
|
enableHeuristicDetection: false,
|
|
702
702
|
enableHeuristicAction: false,
|
|
703
|
+
enablePopupMutationObserver: false,
|
|
703
704
|
detectRetries: 20,
|
|
704
705
|
isMainWorld: false,
|
|
705
706
|
prehideTimeout: 2e3,
|
|
@@ -3703,16 +3704,29 @@
|
|
|
3703
3704
|
this.updateState({ selfTest: selfTestResult });
|
|
3704
3705
|
return selfTestResult;
|
|
3705
3706
|
}
|
|
3706
|
-
// TODO: use MutationObserver like in findCmp()
|
|
3707
3707
|
async waitForPopup(cmp, retries = 10, interval = 500) {
|
|
3708
3708
|
const logsConfig = this.config.logs;
|
|
3709
3709
|
logsConfig.lifecycle && console.log("checking if popup is open...", cmp.name);
|
|
3710
|
+
let mutationObserver = null;
|
|
3711
|
+
if (this.config.enablePopupMutationObserver) {
|
|
3712
|
+
mutationObserver = this.domActions.waitForMutation("html", 1e4);
|
|
3713
|
+
mutationObserver.catch(() => {
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3710
3716
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
3711
3717
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
3712
3718
|
return false;
|
|
3713
3719
|
});
|
|
3714
3720
|
if (!isOpen && retries > 0) {
|
|
3715
|
-
|
|
3721
|
+
if (mutationObserver) {
|
|
3722
|
+
try {
|
|
3723
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
3724
|
+
} catch (e) {
|
|
3725
|
+
logsConfig.lifecycle && console.log(cmp.name, "popup detection timed out waiting for DOM mutation");
|
|
3726
|
+
}
|
|
3727
|
+
} else {
|
|
3728
|
+
await this.domActions.wait(interval);
|
|
3729
|
+
}
|
|
3716
3730
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
3717
3731
|
}
|
|
3718
3732
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? "open" : "not open"}`);
|
package/dist/types/types.d.ts
CHANGED
package/lib/types.ts
CHANGED
|
@@ -65,6 +65,7 @@ export type Config = {
|
|
|
65
65
|
enableFilterList: boolean;
|
|
66
66
|
enableHeuristicDetection: boolean;
|
|
67
67
|
enableHeuristicAction: boolean;
|
|
68
|
+
enablePopupMutationObserver: boolean;
|
|
68
69
|
visualTest: boolean; // If true, the script will delay before every click action
|
|
69
70
|
logs: {
|
|
70
71
|
lifecycle: boolean;
|
package/lib/utils.ts
CHANGED
|
@@ -78,6 +78,7 @@ export function normalizeConfig(providedConfig: any): Config {
|
|
|
78
78
|
enableGeneratedRules: true,
|
|
79
79
|
enableHeuristicDetection: false,
|
|
80
80
|
enableHeuristicAction: false,
|
|
81
|
+
enablePopupMutationObserver: false,
|
|
81
82
|
detectRetries: 20,
|
|
82
83
|
isMainWorld: false,
|
|
83
84
|
prehideTimeout: 2000,
|
package/lib/web.ts
CHANGED
|
@@ -553,16 +553,30 @@ export default class AutoConsent {
|
|
|
553
553
|
return selfTestResult;
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
// TODO: use MutationObserver like in findCmp()
|
|
557
556
|
async waitForPopup(cmp: AutoCMP, retries = 10, interval = 500): Promise<boolean> {
|
|
558
557
|
const logsConfig = this.config.logs;
|
|
559
558
|
logsConfig.lifecycle && console.log('checking if popup is open...', cmp.name);
|
|
559
|
+
|
|
560
|
+
let mutationObserver: Promise<boolean> | null = null;
|
|
561
|
+
if (this.config.enablePopupMutationObserver) {
|
|
562
|
+
mutationObserver = this.domActions.waitForMutation('html', 10000);
|
|
563
|
+
mutationObserver.catch(() => {});
|
|
564
|
+
}
|
|
565
|
+
|
|
560
566
|
const isOpen = await cmp.detectPopup().catch((e) => {
|
|
561
567
|
logsConfig.errors && console.warn(`error detecting popup for ${cmp.name}`, e);
|
|
562
568
|
return false;
|
|
563
|
-
});
|
|
569
|
+
});
|
|
564
570
|
if (!isOpen && retries > 0) {
|
|
565
|
-
|
|
571
|
+
if (mutationObserver) {
|
|
572
|
+
try {
|
|
573
|
+
await Promise.all([this.domActions.wait(interval), mutationObserver]);
|
|
574
|
+
} catch (e) {
|
|
575
|
+
logsConfig.lifecycle && console.log(cmp.name, 'popup detection timed out waiting for DOM mutation');
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
await this.domActions.wait(interval);
|
|
579
|
+
}
|
|
566
580
|
return this.waitForPopup(cmp, retries - 1, interval);
|
|
567
581
|
}
|
|
568
582
|
logsConfig.lifecycle && console.log(cmp.name, `popup is ${isOpen ? 'open' : 'not open'}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<body>
|
|
3
|
+
<script type="module">
|
|
4
|
+
import { runTests } from '@web/test-runner-mocha';
|
|
5
|
+
|
|
6
|
+
runTests(async () => {
|
|
7
|
+
await import('./wait-for-popup');
|
|
8
|
+
});
|
|
9
|
+
</script>
|
|
10
|
+
<div id="page-content">
|
|
11
|
+
<p>Test page for waitForPopup tests</p>
|
|
12
|
+
</div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { expect } from '@esm-bundle/chai';
|
|
2
|
+
import Autoconsent from '../../lib/web';
|
|
3
|
+
import { AutoCMP } from '../../lib/types';
|
|
4
|
+
|
|
5
|
+
function createAutoconsent(enablePopupMutationObserver: boolean): Autoconsent {
|
|
6
|
+
return new Autoconsent(() => Promise.resolve(), {
|
|
7
|
+
enabled: false,
|
|
8
|
+
autoAction: null,
|
|
9
|
+
enablePopupMutationObserver,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createMockCmp(detectPopupFn: () => Promise<boolean>): AutoCMP {
|
|
14
|
+
return {
|
|
15
|
+
name: 'mock-cmp',
|
|
16
|
+
hasSelfTest: false,
|
|
17
|
+
isIntermediate: false,
|
|
18
|
+
isCosmetic: false,
|
|
19
|
+
runContext: { main: true, frame: false },
|
|
20
|
+
checkRunContext: () => true,
|
|
21
|
+
checkFrameContext: () => true,
|
|
22
|
+
hasMatchingUrlPattern: () => true,
|
|
23
|
+
detectCmp: () => Promise.resolve(true),
|
|
24
|
+
detectPopup: detectPopupFn,
|
|
25
|
+
optOut: () => Promise.resolve(true),
|
|
26
|
+
optIn: () => Promise.resolve(true),
|
|
27
|
+
openCmp: () => Promise.resolve(true),
|
|
28
|
+
test: () => Promise.resolve(true),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Autoconsent.waitForPopup', () => {
|
|
33
|
+
describe('without mutation observer (legacy polling)', () => {
|
|
34
|
+
let autoconsent: Autoconsent;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
autoconsent = createAutoconsent(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should resolve true if popup is detected on first check', async () => {
|
|
41
|
+
const cmp = createMockCmp(() => Promise.resolve(true));
|
|
42
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
43
|
+
expect(result).to.be.true;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should resolve false if popup is never detected', async () => {
|
|
47
|
+
const cmp = createMockCmp(() => Promise.resolve(false));
|
|
48
|
+
const result = await autoconsent.waitForPopup(cmp, 2, 50);
|
|
49
|
+
expect(result).to.be.false;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should retry and detect popup that appears later', async () => {
|
|
53
|
+
let callCount = 0;
|
|
54
|
+
const cmp = createMockCmp(() => {
|
|
55
|
+
callCount++;
|
|
56
|
+
return Promise.resolve(callCount >= 3);
|
|
57
|
+
});
|
|
58
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
59
|
+
expect(result).to.be.true;
|
|
60
|
+
expect(callCount).to.equal(3);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle errors in detectPopup gracefully', async () => {
|
|
64
|
+
const cmp = createMockCmp(() => Promise.reject(new Error('detection error')));
|
|
65
|
+
const result = await autoconsent.waitForPopup(cmp, 2, 50);
|
|
66
|
+
expect(result).to.be.false;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should not retry when retries is 0', async () => {
|
|
70
|
+
let callCount = 0;
|
|
71
|
+
const cmp = createMockCmp(() => {
|
|
72
|
+
callCount++;
|
|
73
|
+
return Promise.resolve(false);
|
|
74
|
+
});
|
|
75
|
+
const result = await autoconsent.waitForPopup(cmp, 0, 50);
|
|
76
|
+
expect(result).to.be.false;
|
|
77
|
+
expect(callCount).to.equal(1);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('with mutation observer', () => {
|
|
82
|
+
let autoconsent: Autoconsent;
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
autoconsent = createAutoconsent(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should resolve true if popup is detected on first check', async () => {
|
|
89
|
+
const cmp = createMockCmp(() => Promise.resolve(true));
|
|
90
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
91
|
+
expect(result).to.be.true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should resolve false when retries are exhausted despite mutations', async () => {
|
|
95
|
+
const cmp = createMockCmp(() => Promise.resolve(false));
|
|
96
|
+
|
|
97
|
+
const mutationInterval = setInterval(() => {
|
|
98
|
+
const el = document.createElement('div');
|
|
99
|
+
el.className = 'test-exhaust-mutation';
|
|
100
|
+
document.body.appendChild(el);
|
|
101
|
+
}, 30);
|
|
102
|
+
|
|
103
|
+
const result = await autoconsent.waitForPopup(cmp, 2, 50);
|
|
104
|
+
|
|
105
|
+
clearInterval(mutationInterval);
|
|
106
|
+
document.querySelectorAll('.test-exhaust-mutation').forEach((el) => el.remove());
|
|
107
|
+
|
|
108
|
+
expect(result).to.be.false;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should retry when a DOM mutation occurs and detect popup', async () => {
|
|
112
|
+
let callCount = 0;
|
|
113
|
+
const cmp = createMockCmp(() => {
|
|
114
|
+
callCount++;
|
|
115
|
+
return Promise.resolve(callCount >= 2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// schedule a DOM mutation so the observer resolves
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
const el = document.createElement('span');
|
|
121
|
+
el.id = 'test-mutation-element';
|
|
122
|
+
document.body.appendChild(el);
|
|
123
|
+
}, 30);
|
|
124
|
+
|
|
125
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
126
|
+
expect(result).to.be.true;
|
|
127
|
+
expect(callCount).to.equal(2);
|
|
128
|
+
|
|
129
|
+
document.getElementById('test-mutation-element')?.remove();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should detect popup that appears after multiple mutations', async () => {
|
|
133
|
+
let callCount = 0;
|
|
134
|
+
const cmp = createMockCmp(() => {
|
|
135
|
+
callCount++;
|
|
136
|
+
return Promise.resolve(callCount >= 3);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// schedule DOM mutations to keep the observer firing
|
|
140
|
+
const mutationInterval = setInterval(() => {
|
|
141
|
+
const el = document.createElement('div');
|
|
142
|
+
el.className = 'test-multi-mutation';
|
|
143
|
+
document.body.appendChild(el);
|
|
144
|
+
}, 40);
|
|
145
|
+
|
|
146
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
147
|
+
|
|
148
|
+
clearInterval(mutationInterval);
|
|
149
|
+
document.querySelectorAll('.test-multi-mutation').forEach((el) => el.remove());
|
|
150
|
+
|
|
151
|
+
expect(result).to.be.true;
|
|
152
|
+
expect(callCount).to.equal(3);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle errors in detectPopup gracefully', async () => {
|
|
156
|
+
let callCount = 0;
|
|
157
|
+
const cmp = createMockCmp(() => {
|
|
158
|
+
callCount++;
|
|
159
|
+
if (callCount <= 2) {
|
|
160
|
+
return Promise.reject(new Error('transient error'));
|
|
161
|
+
}
|
|
162
|
+
return Promise.resolve(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// schedule mutations so retries proceed
|
|
166
|
+
const mutationInterval = setInterval(() => {
|
|
167
|
+
const el = document.createElement('div');
|
|
168
|
+
el.className = 'test-error-mutation';
|
|
169
|
+
document.body.appendChild(el);
|
|
170
|
+
}, 30);
|
|
171
|
+
|
|
172
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
173
|
+
|
|
174
|
+
clearInterval(mutationInterval);
|
|
175
|
+
document.querySelectorAll('.test-error-mutation').forEach((el) => el.remove());
|
|
176
|
+
|
|
177
|
+
expect(result).to.be.true;
|
|
178
|
+
expect(callCount).to.equal(3);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should not retry when retries is 0', async () => {
|
|
182
|
+
let callCount = 0;
|
|
183
|
+
const cmp = createMockCmp(() => {
|
|
184
|
+
callCount++;
|
|
185
|
+
return Promise.resolve(false);
|
|
186
|
+
});
|
|
187
|
+
const result = await autoconsent.waitForPopup(cmp, 0, 50);
|
|
188
|
+
expect(result).to.be.false;
|
|
189
|
+
expect(callCount).to.equal(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should continue retrying after mutation observer timeout', async () => {
|
|
193
|
+
// Override waitForMutation to use a very short timeout so it rejects quickly
|
|
194
|
+
const originalWaitForMutation = autoconsent.domActions.waitForMutation.bind(autoconsent.domActions);
|
|
195
|
+
autoconsent.domActions.waitForMutation = (selector, _timeout?) => {
|
|
196
|
+
return originalWaitForMutation(selector, 10);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let callCount = 0;
|
|
200
|
+
const cmp = createMockCmp(() => {
|
|
201
|
+
callCount++;
|
|
202
|
+
// popup appears on the 3rd attempt (after 2 mutation timeouts)
|
|
203
|
+
return Promise.resolve(callCount >= 3);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = await autoconsent.waitForPopup(cmp, 5, 50);
|
|
207
|
+
|
|
208
|
+
autoconsent.domActions.waitForMutation = originalWaitForMutation;
|
|
209
|
+
|
|
210
|
+
expect(result).to.be.true;
|
|
211
|
+
expect(callCount).to.equal(3);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|