@duckduckgo/autoconsent 14.97.0 → 15.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 (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/addon-firefox/background.bundle.js +10 -1
  3. package/dist/addon-firefox/compact-rules.json +1 -1
  4. package/dist/addon-firefox/content.bundle.js +142 -33
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-firefox/popup.bundle.js +10 -1
  7. package/dist/addon-firefox/rules.json +1 -1
  8. package/dist/addon-mv3/background.bundle.js +10 -1
  9. package/dist/addon-mv3/compact-rules.json +1 -1
  10. package/dist/addon-mv3/content.bundle.js +142 -33
  11. package/dist/addon-mv3/manifest.json +1 -1
  12. package/dist/addon-mv3/popup.bundle.js +10 -1
  13. package/dist/addon-mv3/rules.json +1 -1
  14. package/dist/autoconsent.cjs.js +142 -33
  15. package/dist/autoconsent.esm.js +142 -33
  16. package/dist/autoconsent.extra.cjs.js +142 -33
  17. package/dist/autoconsent.extra.esm.js +142 -33
  18. package/dist/autoconsent.playwright.js +142 -33
  19. package/dist/types/cmps/base.d.ts +4 -2
  20. package/dist/types/heuristic-patterns.d.ts +3 -0
  21. package/dist/types/heuristics.d.ts +4 -7
  22. package/dist/types/types.d.ts +18 -2
  23. package/lib/cmps/base.ts +20 -7
  24. package/lib/heuristic-patterns.ts +68 -1
  25. package/lib/heuristics.ts +76 -27
  26. package/lib/types.ts +20 -2
  27. package/lib/utils.ts +2 -1
  28. package/lib/web.ts +3 -1
  29. package/package.json +1 -1
  30. package/rules/autoconsent/twitch.json +1 -1
  31. package/rules/compact-rules.json +1 -1
  32. package/rules/rules.json +1 -1
  33. package/tests-wtr/heuristics/get-actionable-popups.html +16 -0
  34. package/tests-wtr/heuristics/get-actionable-popups.ts +85 -6
  35. package/tests-wtr/heuristics/heuristic-cmp.html +69 -0
  36. package/tests-wtr/heuristics/heuristic-cmp.ts +178 -0
  37. package/tests-wtr/heuristics/heuristics-utils.test.ts +96 -28
  38. package/tests-wtr/lifecycle/find-cmp.html +14 -0
  39. package/tests-wtr/lifecycle/find-cmp.ts +44 -2
@@ -92,5 +92,21 @@
92
92
  <button>Accept All</button>
93
93
  <button>Reject All</button>
94
94
  </dialog>
95
+
96
+ <div id="popup-accept-only" class="popup">
97
+ <p>We use cookies to improve your experience.</p>
98
+ <button>Accept All</button>
99
+ </div>
100
+
101
+ <div id="popup-acknowledge-only" class="popup">
102
+ <p>We use cookies to improve your experience.</p>
103
+ <button>OK</button>
104
+ </div>
105
+
106
+ <div id="popup-multiple-reject" class="popup">
107
+ <p>We use cookies to improve your experience.</p>
108
+ <button>Reject All</button>
109
+ <button>Deny</button>
110
+ </div>
95
111
  </body>
96
112
  </html>
@@ -1,5 +1,14 @@
1
1
  import { expect } from '@esm-bundle/chai';
2
2
  import { getActionablePopups } from '../../lib/heuristics';
3
+ import { ButtonData, PopupHandlingModes } from '../../lib/types';
4
+
5
+ function rejectButtons(buttons: ButtonData[]) {
6
+ return buttons.filter((b) => b.regexClassification === 'reject');
7
+ }
8
+
9
+ function nonRejectButtons(buttons: ButtonData[]) {
10
+ return buttons.filter((b) => b.regexClassification !== 'reject');
11
+ }
3
12
 
4
13
  // must be run from get-actionable-popups.html
5
14
  describe('getActionablePopups', () => {
@@ -21,8 +30,8 @@ describe('getActionablePopups', () => {
21
30
  const popups = getActionablePopups();
22
31
 
23
32
  expect(popups.length).to.equal(1);
24
- expect(popups[0].rejectButtons?.length).to.equal(1);
25
- expect(popups[0].rejectButtons?.[0].text).to.equal('Reject All');
33
+ expect(rejectButtons(popups[0].buttons)).to.have.length(1);
34
+ expect(rejectButtons(popups[0].buttons)[0].text).to.equal('Reject All');
26
35
  });
27
36
 
28
37
  describe('does not match non-cookie popups', () => {
@@ -34,13 +43,83 @@ describe('getActionablePopups', () => {
34
43
  expect(popups.length).to.equal(0);
35
44
  });
36
45
 
37
- it('ignores popup without reject button', () => {
46
+ it('ignores popup with accept and settings buttons', () => {
38
47
  showPopup('popup-no-reject');
39
48
 
40
- const popups = getActionablePopups();
49
+ const popups = getActionablePopups(PopupHandlingModes.Tier2);
50
+
51
+ expect(popups.length).to.equal(0);
52
+ });
53
+ });
54
+
55
+ describe('tier button candidates', () => {
56
+ it('finds accept-only popup with no reject buttons', () => {
57
+ showPopup('popup-accept-only');
58
+
59
+ const popups = getActionablePopups(PopupHandlingModes.Tier2);
60
+
61
+ expect(popups.length).to.equal(1);
62
+ expect(rejectButtons(popups[0].buttons)).to.have.length(0);
63
+ expect(popups[0].buttons.filter((b) => b.regexClassification === 'accept')).to.have.length(1);
64
+ });
65
+
66
+ it('finds acknowledge-only popup with no reject buttons', () => {
67
+ showPopup('popup-acknowledge-only');
68
+
69
+ const popups = getActionablePopups(PopupHandlingModes.Tier2);
70
+
71
+ expect(popups.length).to.equal(1);
72
+ expect(rejectButtons(popups[0].buttons)).to.have.length(0);
73
+ expect(popups[0].buttons.filter((b) => b.regexClassification === 'acknowledge')).to.have.length(1);
74
+ });
75
+
76
+ it('finds popup with multiple reject buttons', () => {
77
+ showPopup('popup-multiple-reject');
78
+
79
+ const popups = getActionablePopups(PopupHandlingModes.Tier2);
80
+
81
+ expect(popups.length).to.equal(1);
82
+ expect(popups[0].regexClassification).to.equal(PopupHandlingModes.Reject);
83
+ expect(rejectButtons(popups[0].buttons)).to.have.length(2);
84
+ });
85
+ });
86
+
87
+ describe('mode filtering', () => {
88
+ it('does not return accept-only popup when mode is Reject', () => {
89
+ showPopup('popup-accept-only');
90
+
91
+ const popups = getActionablePopups(PopupHandlingModes.Reject);
92
+
93
+ expect(popups.length).to.equal(0);
94
+ });
95
+
96
+ it('returns acknowledge-only popup when mode is Tier1', () => {
97
+ showPopup('popup-acknowledge-only');
98
+
99
+ const popups = getActionablePopups(PopupHandlingModes.Tier1);
100
+
101
+ expect(popups.length).to.equal(1);
102
+ expect(popups[0].regexClassification).to.equal(PopupHandlingModes.Tier1);
103
+ });
104
+
105
+ it('does not return accept-only popup when mode is Tier1', () => {
106
+ showPopup('popup-accept-only');
107
+
108
+ const popups = getActionablePopups(PopupHandlingModes.Tier1);
41
109
 
42
110
  expect(popups.length).to.equal(0);
43
111
  });
112
+
113
+ it('prefers reject popup when reject and accept popups are visible', () => {
114
+ showPopup('popup-reject-all');
115
+ showPopup('popup-accept-only');
116
+
117
+ const popups = getActionablePopups(PopupHandlingModes.Tier2);
118
+
119
+ expect(popups.length).to.equal(2);
120
+ expect(popups[0].regexClassification).to.equal(PopupHandlingModes.Reject);
121
+ expect(popups[1].regexClassification).to.equal(PopupHandlingModes.Tier2);
122
+ });
44
123
  });
45
124
 
46
125
  describe('positioning', () => {
@@ -112,8 +191,8 @@ describe('getActionablePopups', () => {
112
191
  const popups = getActionablePopups();
113
192
 
114
193
  expect(popups.length).to.equal(1);
115
- expect(popups[0].rejectButtons?.length).to.equal(1);
116
- expect(popups[0].otherButtons?.length).to.equal(3);
194
+ expect(rejectButtons(popups[0].buttons)).to.have.length(1);
195
+ expect(nonRejectButtons(popups[0].buttons)).to.have.length(3);
117
196
  });
118
197
  });
119
198
  });
@@ -0,0 +1,69 @@
1
+ <html>
2
+ <head>
3
+ <style>
4
+ .popup {
5
+ position: fixed;
6
+ top: 0;
7
+ left: 0;
8
+ width: 300px;
9
+ height: 200px;
10
+ background: white;
11
+ display: none;
12
+ }
13
+ .popup.visible {
14
+ display: block;
15
+ }
16
+ .popup.tier-preference-accept {
17
+ top: 0;
18
+ left: 320px;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <script type="module">
24
+ import { runTests } from '@web/test-runner-mocha';
25
+
26
+ runTests(async () => {
27
+ await import('./heuristic-cmp');
28
+ });
29
+ </script>
30
+
31
+ <div id="popup-reject" class="popup">
32
+ <p>We use cookies to improve your experience.</p>
33
+ <button>Accept All</button>
34
+ <button>Reject All</button>
35
+ </div>
36
+
37
+ <div id="popup-acknowledge-only" class="popup">
38
+ <p>We use cookies to improve your experience.</p>
39
+ <button>OK</button>
40
+ </div>
41
+
42
+ <div id="popup-accept-only" class="popup">
43
+ <p>We use cookies to improve your experience.</p>
44
+ <button>Accept All</button>
45
+ </div>
46
+
47
+ <div id="popup-accept-settings" class="popup">
48
+ <p>We use cookies to improve your experience.</p>
49
+ <button>Accept All</button>
50
+ <button>Settings</button>
51
+ </div>
52
+
53
+ <div id="popup-multiple-reject" class="popup">
54
+ <p>We use cookies to improve your experience.</p>
55
+ <button>Reject All</button>
56
+ <button>Deny</button>
57
+ </div>
58
+
59
+ <div id="popup-tier-preference-reject" class="popup">
60
+ <p>We use cookies to improve your experience.</p>
61
+ <button>Reject All</button>
62
+ </div>
63
+
64
+ <div id="popup-tier-preference-accept" class="popup tier-preference-accept">
65
+ <p>We use cookies to improve your experience.</p>
66
+ <button>Accept All</button>
67
+ </div>
68
+ </body>
69
+ </html>
@@ -0,0 +1,178 @@
1
+ import { expect } from '@esm-bundle/chai';
2
+ import Autoconsent from '../../lib/web';
3
+ import { AutoConsentHeuristicCMP } from '../../lib/cmps/base';
4
+ import { PopupHandlingMode, PopupHandlingModes } from '../../lib/types';
5
+
6
+ // must be run from heuristic-cmp.html
7
+ describe('AutoConsentHeuristicCMP', () => {
8
+ function createAutoconsent(heuristicMode: PopupHandlingMode) {
9
+ return new Autoconsent(() => Promise.resolve(), {
10
+ enabled: false,
11
+ autoAction: null,
12
+ enableHeuristicAction: true,
13
+ heuristicMode,
14
+ });
15
+ }
16
+
17
+ function showPopup(...ids: string[]) {
18
+ hideAllPopups();
19
+ for (const id of ids) {
20
+ document.getElementById(id)?.classList.add('visible');
21
+ }
22
+ }
23
+
24
+ function hideAllPopups() {
25
+ document.querySelectorAll('.popup').forEach((el) => el.classList.remove('visible'));
26
+ }
27
+
28
+ afterEach(() => {
29
+ hideAllPopups();
30
+ });
31
+
32
+ describe('detectCmp', () => {
33
+ it('detects reject popup as HEURISTIC-TIER0', async () => {
34
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
35
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
36
+ showPopup('popup-reject');
37
+
38
+ const detected = await cmp.detectCmp();
39
+
40
+ expect(detected).to.be.true;
41
+ expect(cmp.name).to.equal('HEURISTIC-TIER0');
42
+ });
43
+
44
+ it('detects acknowledge-only popup as HEURISTIC-TIER1', async () => {
45
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
46
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
47
+ showPopup('popup-acknowledge-only');
48
+
49
+ const detected = await cmp.detectCmp();
50
+
51
+ expect(detected).to.be.true;
52
+ expect(cmp.name).to.equal('HEURISTIC-TIER1');
53
+ });
54
+
55
+ it('detects accept-only popup as HEURISTIC-TIER2', async () => {
56
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
57
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
58
+ showPopup('popup-accept-only');
59
+
60
+ const detected = await cmp.detectCmp();
61
+
62
+ expect(detected).to.be.true;
63
+ expect(cmp.name).to.equal('HEURISTIC-TIER2');
64
+ });
65
+
66
+ it('does not detect popup with accept and settings buttons', async () => {
67
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
68
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
69
+ showPopup('popup-accept-settings');
70
+
71
+ const detected = await cmp.detectCmp();
72
+
73
+ expect(detected).to.be.false;
74
+ });
75
+
76
+ it('detects popup with multiple reject buttons as HEURISTIC-TIER0', async () => {
77
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
78
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
79
+ showPopup('popup-multiple-reject');
80
+
81
+ const detected = await cmp.detectCmp();
82
+
83
+ expect(detected).to.be.true;
84
+ expect(cmp.name).to.equal('HEURISTIC-TIER0');
85
+ });
86
+
87
+ it('prefers reject popup when reject and accept popups are visible', async () => {
88
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
89
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
90
+ showPopup('popup-tier-preference-reject', 'popup-tier-preference-accept');
91
+
92
+ const detected = await cmp.detectCmp();
93
+
94
+ expect(detected).to.be.true;
95
+ expect(cmp.name).to.equal('HEURISTIC-TIER0');
96
+ });
97
+ });
98
+
99
+ describe('heuristicMode cap', () => {
100
+ it('does not detect accept-only popup when mode is Reject', async () => {
101
+ const autoconsent = createAutoconsent(PopupHandlingModes.Reject);
102
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Reject);
103
+ showPopup('popup-accept-only');
104
+
105
+ const detected = await cmp.detectCmp();
106
+
107
+ expect(detected).to.be.false;
108
+ });
109
+
110
+ it('detects acknowledge-only popup when mode is Tier1', async () => {
111
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier1);
112
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier1);
113
+ showPopup('popup-acknowledge-only');
114
+
115
+ const detected = await cmp.detectCmp();
116
+
117
+ expect(detected).to.be.true;
118
+ expect(cmp.name).to.equal('HEURISTIC-TIER1');
119
+ });
120
+
121
+ it('does not detect accept-only popup when mode is Tier1', async () => {
122
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier1);
123
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier1);
124
+ showPopup('popup-accept-only');
125
+
126
+ const detected = await cmp.detectCmp();
127
+
128
+ expect(detected).to.be.false;
129
+ });
130
+ });
131
+
132
+ describe('optOut', () => {
133
+ it('clicks the reject button for TIER0 popup', async () => {
134
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
135
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
136
+ showPopup('popup-reject');
137
+ await cmp.detectCmp();
138
+
139
+ const target = cmp.getTargetButton();
140
+ expect(target?.regexClassification).to.equal('reject');
141
+
142
+ const result = await cmp.optOut();
143
+
144
+ expect(result).to.be.true;
145
+ expect(autoconsent.state.clicks).to.equal(1);
146
+ });
147
+
148
+ it('clicks the acknowledge button for TIER1 popup', async () => {
149
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
150
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
151
+ showPopup('popup-acknowledge-only');
152
+ await cmp.detectCmp();
153
+
154
+ const target = cmp.getTargetButton();
155
+ expect(target?.regexClassification).to.equal('acknowledge');
156
+
157
+ const result = await cmp.optOut();
158
+
159
+ expect(result).to.be.true;
160
+ expect(autoconsent.state.clicks).to.equal(1);
161
+ });
162
+
163
+ it('clicks the accept button for TIER2 popup', async () => {
164
+ const autoconsent = createAutoconsent(PopupHandlingModes.Tier2);
165
+ const cmp = new AutoConsentHeuristicCMP(autoconsent, PopupHandlingModes.Tier2);
166
+ showPopup('popup-accept-only');
167
+ await cmp.detectCmp();
168
+
169
+ const target = cmp.getTargetButton();
170
+ expect(target?.regexClassification).to.equal('accept');
171
+
172
+ const result = await cmp.optOut();
173
+
174
+ expect(result).to.be.true;
175
+ expect(autoconsent.state.clicks).to.equal(1);
176
+ });
177
+ });
178
+ });
@@ -2,13 +2,22 @@ import { expect } from '@esm-bundle/chai';
2
2
  import {
3
3
  checkHeuristicPatterns,
4
4
  cleanButtonText,
5
- isRejectButton,
5
+ classifyButtonTextRegex,
6
6
  classifyButtons,
7
7
  isDisabled,
8
8
  excludeContainers,
9
9
  getButtonData,
10
10
  isDialogLikeElement,
11
11
  } from '../../lib/heuristics';
12
+ import { ButtonData } from '../../lib/types';
13
+
14
+ function rejectButtons(buttons: ButtonData[]) {
15
+ return buttons.filter((b) => b.regexClassification === 'reject');
16
+ }
17
+
18
+ function nonRejectButtons(buttons: ButtonData[]) {
19
+ return buttons.filter((b) => b.regexClassification !== 'reject');
20
+ }
12
21
 
13
22
  describe('checkHeuristicPatterns', () => {
14
23
  it('detects cookie-related text', () => {
@@ -66,69 +75,128 @@ describe('cleanButtonText', () => {
66
75
  });
67
76
  });
68
77
 
69
- describe('isRejectButton', () => {
78
+ describe('classifyButtonTextRegex', () => {
70
79
  it('matches reject patterns', () => {
71
- expect(isRejectButton('let us Reject All cookies', [/reject all/gi])).to.be.true;
80
+ expect(classifyButtonTextRegex('Reject All')).to.equal('reject');
72
81
  });
73
82
 
74
83
  it('does not match never match patterns', () => {
75
- expect(isRejectButton('let us Reject All cookies', [/reject all/gi], [/let us/gi])).to.be.false;
84
+ expect(classifyButtonTextRegex('reject and pay')).to.equal('other');
76
85
  });
77
86
 
78
87
  it('matches "except strictly necessary" qualifier', () => {
79
88
  // OneTrust on apnews.com labels the reject button "I Reject All (except Strictly Necessary)"
80
- expect(isRejectButton('I Reject All (except Strictly Necessary)')).to.be.true;
81
- expect(isRejectButton('Reject All (except Strictly Necessary)')).to.be.true;
82
- expect(isRejectButton('Reject All (except Necessary)')).to.be.true;
83
- expect(isRejectButton('Deny All (except Strictly Essential)')).to.be.true;
84
- expect(isRejectButton('I Reject All except necessary')).to.be.true;
89
+ expect(classifyButtonTextRegex('I Reject All (except Strictly Necessary)')).to.equal('reject');
90
+ expect(classifyButtonTextRegex('Reject All (except Strictly Necessary)')).to.equal('reject');
91
+ expect(classifyButtonTextRegex('Reject All (except Necessary)')).to.equal('reject');
92
+ expect(classifyButtonTextRegex('Deny All (except Strictly Essential)')).to.equal('reject');
93
+ expect(classifyButtonTextRegex('I Reject All except necessary')).to.equal('reject');
85
94
  });
86
95
 
87
- it('returns false for empty string', () => {
88
- expect(isRejectButton('')).to.be.false;
96
+ it('returns other for empty string', () => {
97
+ expect(classifyButtonTextRegex('')).to.equal('other');
89
98
  });
90
99
 
91
- it('supports exact strings', () => {
92
- expect(isRejectButton('let us reject all', ['let us reject all'])).to.be.true;
100
+ it('supports exact string patterns', () => {
101
+ expect(classifyButtonTextRegex('no')).to.equal('reject');
93
102
  });
94
103
 
95
- it('does not match substrings in case of exact strings', () => {
96
- expect(isRejectButton('let us reject all', ['reject'])).to.be.false;
104
+ it('does not match partial exact string patterns', () => {
105
+ expect(classifyButtonTextRegex('no problem')).to.equal('other');
97
106
  });
98
107
  });
99
108
 
100
109
  describe('classifyButtons', () => {
101
110
  it('separates reject buttons from other buttons', () => {
102
- const buttons = [
111
+ const buttons: ButtonData[] = [
103
112
  { text: 'Accept All', element: document.createElement('button') },
104
113
  { text: 'Reject All', element: document.createElement('button') },
105
114
  { text: 'Settings', element: document.createElement('button') },
106
115
  ];
107
116
 
108
- const { rejectButtons, otherButtons } = classifyButtons(buttons);
117
+ classifyButtons(buttons);
109
118
 
110
- expect(rejectButtons).to.have.length(1);
111
- expect(rejectButtons[0].text).to.equal('Reject All');
112
- expect(otherButtons).to.have.length(2);
119
+ expect(rejectButtons(buttons)).to.have.length(1);
120
+ expect(rejectButtons(buttons)[0].text).to.equal('Reject All');
121
+ expect(nonRejectButtons(buttons)).to.have.length(2);
113
122
  });
114
123
 
115
- it('returns empty arrays for empty input', () => {
116
- const { rejectButtons, otherButtons } = classifyButtons([]);
124
+ it('handles empty input', () => {
125
+ const buttons: ButtonData[] = [];
126
+
127
+ classifyButtons(buttons);
117
128
 
118
- expect(rejectButtons).to.have.length(0);
119
- expect(otherButtons).to.have.length(0);
129
+ expect(rejectButtons(buttons)).to.have.length(0);
130
+ expect(nonRejectButtons(buttons)).to.have.length(0);
120
131
  });
121
132
 
122
133
  it('handles multiple reject buttons', () => {
123
- const buttons = [
134
+ const buttons: ButtonData[] = [
124
135
  { text: 'Reject All', element: document.createElement('button') },
125
136
  { text: 'Deny', element: document.createElement('button') },
126
137
  ];
127
138
 
128
- const { rejectButtons, otherButtons } = classifyButtons(buttons);
139
+ classifyButtons(buttons);
140
+
141
+ expect(rejectButtons(buttons)).to.have.length(2);
142
+ expect(nonRejectButtons(buttons)).to.have.length(0);
143
+ });
144
+
145
+ it('sets regexClassification on all buttons', () => {
146
+ const buttons: ButtonData[] = [
147
+ { text: 'Accept All', element: document.createElement('button') },
148
+ { text: 'Reject All', element: document.createElement('button') },
149
+ { text: 'Settings', element: document.createElement('button') },
150
+ ];
151
+
152
+ classifyButtons(buttons);
153
+
154
+ expect(buttons.every((b) => b.regexClassification)).to.be.true;
155
+ });
156
+
157
+ it('classifies accept buttons', () => {
158
+ const buttons: ButtonData[] = [{ text: 'Accept All', element: document.createElement('button') }];
159
+
160
+ classifyButtons(buttons);
161
+
162
+ expect(rejectButtons(buttons)).to.have.length(0);
163
+ expect(buttons).to.have.length(1);
164
+ expect(buttons[0].regexClassification).to.equal('accept');
165
+ });
166
+
167
+ it('classifies acknowledge buttons', () => {
168
+ const buttons: ButtonData[] = [
169
+ { text: 'OK', element: document.createElement('button') },
170
+ { text: 'I understand', element: document.createElement('button') },
171
+ ];
172
+
173
+ classifyButtons(buttons);
174
+
175
+ expect(rejectButtons(buttons)).to.have.length(0);
176
+ expect(buttons).to.have.length(2);
177
+ expect(buttons.every((b) => b.regexClassification === 'acknowledge')).to.be.true;
178
+ });
179
+
180
+ it('classifies settings buttons', () => {
181
+ const buttons: ButtonData[] = [
182
+ { text: 'Settings', element: document.createElement('button') },
183
+ { text: 'Cookie preferences', element: document.createElement('button') },
184
+ ];
185
+
186
+ classifyButtons(buttons);
187
+
188
+ expect(rejectButtons(buttons)).to.have.length(0);
189
+ expect(buttons).to.have.length(2);
190
+ expect(buttons.every((b) => b.regexClassification === 'settings')).to.be.true;
191
+ });
192
+
193
+ it('classifies reject buttons with regexClassification', () => {
194
+ const buttons: ButtonData[] = [{ text: 'Reject All', element: document.createElement('button') }];
195
+
196
+ classifyButtons(buttons);
129
197
 
130
- expect(rejectButtons).to.have.length(2);
131
- expect(otherButtons).to.have.length(0);
198
+ expect(rejectButtons(buttons)).to.have.length(1);
199
+ expect(rejectButtons(buttons)[0].regexClassification).to.equal('reject');
132
200
  });
133
201
  });
134
202
 
@@ -19,6 +19,20 @@
19
19
  <button>Accept All</button>
20
20
  <button>Reject All</button>
21
21
  </div>
22
+ <div
23
+ id="heuristic-popup-acknowledge-only"
24
+ style="position: fixed; top: 0; left: 0; width: 300px; height: 200px; background: white; display: none"
25
+ >
26
+ <p>We use cookies to improve your experience.</p>
27
+ <button>OK</button>
28
+ </div>
29
+ <div
30
+ id="heuristic-popup-accept-only"
31
+ style="position: fixed; top: 0; left: 0; width: 300px; height: 200px; background: white; display: none"
32
+ >
33
+ <p>We use cookies to improve your experience.</p>
34
+ <button>Accept All</button>
35
+ </div>
22
36
  <div id="onetrust-pc-sdk" style="display: none">Cookie Preference Center</div>
23
37
  </body>
24
38
  </html>
@@ -1,6 +1,7 @@
1
1
  import { expect } from '@esm-bundle/chai';
2
2
  import Autoconsent from '../../lib/web';
3
3
  import Onetrust from '../../lib/cmps/onetrust';
4
+ import { PopupHandlingModes } from '../../lib/types';
4
5
 
5
6
  describe('Autoconsent.findCmp', () => {
6
7
  let autoconsent: Autoconsent;
@@ -145,7 +146,12 @@ describe('Autoconsent.findCmp', () => {
145
146
  });
146
147
 
147
148
  afterEach(() => {
148
- document.getElementById('heuristic-popup')?.remove();
149
+ const heuristicPopup = document.getElementById('heuristic-popup');
150
+ if (heuristicPopup) {
151
+ heuristicPopup.style.display = '';
152
+ }
153
+ document.getElementById('heuristic-popup-acknowledge-only')!.style.display = 'none';
154
+ document.getElementById('heuristic-popup-accept-only')!.style.display = 'none';
149
155
  });
150
156
 
151
157
  it('returns heuristic CMP when no declarative rules match', async () => {
@@ -161,7 +167,7 @@ describe('Autoconsent.findCmp', () => {
161
167
  const found = await autoconsent.findCmp(1);
162
168
 
163
169
  expect(found).to.have.length(1);
164
- expect(found[0].name).to.equal('HEURISTIC');
170
+ expect(found[0].name).to.equal('HEURISTIC-TIER0');
165
171
  });
166
172
 
167
173
  it('prefers declarative rules over heuristic CMP', async () => {
@@ -178,5 +184,41 @@ describe('Autoconsent.findCmp', () => {
178
184
  expect(found).to.have.length(1);
179
185
  expect(found[0].name).to.equal('test');
180
186
  });
187
+
188
+ it('returns HEURISTIC-TIER1 for acknowledge-only popup when mode is Tier1', async () => {
189
+ document.getElementById('heuristic-popup')!.style.display = 'none';
190
+ document.getElementById('heuristic-popup-acknowledge-only')!.style.display = 'block';
191
+
192
+ autoconsent = new Autoconsent((msg) => Promise.resolve(), {
193
+ enabled: false,
194
+ autoAction: null,
195
+ enableHeuristicAction: true,
196
+ heuristicMode: PopupHandlingModes.Tier1,
197
+ });
198
+ autoconsent.state.findCmpAttempts = 1;
199
+
200
+ const found = await autoconsent.findCmp(1);
201
+
202
+ expect(found).to.have.length(1);
203
+ expect(found[0].name).to.equal('HEURISTIC-TIER1');
204
+ });
205
+
206
+ it('returns HEURISTIC-TIER2 for accept-only popup when mode is Tier2', async () => {
207
+ document.getElementById('heuristic-popup')!.style.display = 'none';
208
+ document.getElementById('heuristic-popup-accept-only')!.style.display = 'block';
209
+
210
+ autoconsent = new Autoconsent((msg) => Promise.resolve(), {
211
+ enabled: false,
212
+ autoAction: null,
213
+ enableHeuristicAction: true,
214
+ heuristicMode: PopupHandlingModes.Tier2,
215
+ });
216
+ autoconsent.state.findCmpAttempts = 1;
217
+
218
+ const found = await autoconsent.findCmp(1);
219
+
220
+ expect(found).to.have.length(1);
221
+ expect(found[0].name).to.equal('HEURISTIC-TIER2');
222
+ });
181
223
  });
182
224
  });