@duckduckgo/autoconsent 16.63.0 → 17.1.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/CHANGELOG.md +98 -0
- package/dist/addon-firefox/background.bundle.js +1 -0
- package/dist/addon-firefox/compact-rules.json +1 -1
- package/dist/addon-firefox/content.bundle.js +177 -38
- package/dist/addon-firefox/manifest.json +1 -1
- package/dist/addon-firefox/popup.bundle.js +1 -0
- package/dist/addon-firefox/rule-index.json +1 -1
- package/dist/addon-firefox/rules.json +1 -1
- package/dist/addon-mv3/background.bundle.js +1 -0
- package/dist/addon-mv3/compact-rules.json +1 -1
- package/dist/addon-mv3/content.bundle.js +177 -38
- package/dist/addon-mv3/manifest.json +1 -1
- package/dist/addon-mv3/popup.bundle.js +1 -0
- package/dist/addon-mv3/rule-index.json +1 -1
- package/dist/addon-mv3/rules.json +1 -1
- package/dist/autoconsent.cjs.js +177 -38
- package/dist/autoconsent.esm.js +177 -38
- package/dist/autoconsent.playwright.js +180 -38
- package/dist/autoconsent.standalone.js +178 -39
- package/dist/heuristics.cjs.js +15 -3
- package/dist/heuristics.mjs +15 -3
- package/dist/types/heuristics.d.ts +12 -3
- package/dist/types/types.d.ts +12 -1
- package/lib/cmps/base.ts +5 -1
- package/lib/heuristic-patterns.ts +18 -2
- package/lib/heuristics.ts +240 -45
- package/lib/types.ts +16 -1
- package/lib/utils.ts +1 -0
- package/package.json +1 -1
- package/readme.md +1 -1
- package/rules/autoconsent/1pmobile.json +44 -0
- package/rules/autoconsent/amasty-gdpr-hyva.json +131 -0
- package/rules/autoconsent/amazonforum.json +86 -0
- package/rules/autoconsent/asrock.json +44 -0
- package/rules/autoconsent/babylist.json +23 -0
- package/rules/autoconsent/babylonbee.json +36 -0
- package/rules/autoconsent/bathnes-cookie-banner.json +31 -0
- package/rules/autoconsent/chairish.json +17 -0
- package/rules/autoconsent/chemicalbook.json +13 -0
- package/rules/autoconsent/chimnie-co-uk.json +21 -0
- package/rules/autoconsent/complianz-optin.json +5 -3
- package/rules/autoconsent/consentmo.json +5 -2
- package/rules/autoconsent/cookie-pref-form.json +48 -0
- package/rules/autoconsent/cookie-script.json +34 -15
- package/rules/autoconsent/endeavorhealth.json +31 -0
- package/rules/autoconsent/ensighten.json +3 -3
- package/rules/autoconsent/epaas.json +18 -1
- package/rules/autoconsent/fepblue-org.json +13 -0
- package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
- package/rules/autoconsent/fod-fujitv.json +13 -0
- package/rules/autoconsent/fpl-com.json +34 -0
- package/rules/autoconsent/fuqer.json +11 -0
- package/rules/autoconsent/greatschools.json +13 -0
- package/rules/autoconsent/hamradio-com.json +13 -0
- package/rules/autoconsent/hkdir.json +33 -0
- package/rules/autoconsent/hubspot.json +7 -1
- package/rules/autoconsent/ibkr.json +16 -0
- package/rules/autoconsent/jetpens.json +44 -0
- package/rules/autoconsent/jokul-cookie-consent.json +28 -0
- package/rules/autoconsent/kanopy.json +88 -0
- package/rules/autoconsent/lego.json +27 -5
- package/rules/autoconsent/mainlinemenswear.json +39 -0
- package/rules/autoconsent/mars-cookies-prompt.json +20 -0
- package/rules/autoconsent/money-co-uk.json +21 -0
- package/rules/autoconsent/myjobquote.json +19 -0
- package/rules/autoconsent/niedziela-pl.json +35 -0
- package/rules/autoconsent/nikkeibp.json +10 -0
- package/rules/autoconsent/northwell-edu.json +29 -0
- package/rules/autoconsent/nts-org-uk.json +20 -0
- package/rules/autoconsent/nyu-langone.json +30 -0
- package/rules/autoconsent/ofsted-reports-cosmetic.json +13 -0
- package/rules/autoconsent/onecom-cookie-banner.json +25 -0
- package/rules/autoconsent/optum-legal-banner.json +10 -0
- package/rules/autoconsent/optum.json +35 -0
- package/rules/autoconsent/oslosportslager.no.json +34 -0
- package/rules/autoconsent/planned-parenthood.json +17 -0
- package/rules/autoconsent/plex.tv.json +52 -0
- package/rules/autoconsent/pornhub-compact-cookie-banner.json +1 -1
- package/rules/autoconsent/pornhub.json +1 -1
- package/rules/autoconsent/radio-canada.json +28 -0
- package/rules/autoconsent/regjeringen.no.json +13 -0
- package/rules/autoconsent/reviewsolicitors.json +41 -0
- package/rules/autoconsent/ring.json +1 -1
- package/rules/autoconsent/roll20.json +50 -0
- package/rules/autoconsent/sankei-digital.json +37 -0
- package/rules/autoconsent/sirdata.json +8 -3
- package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
- package/rules/autoconsent/sparebank1-optin.json +10 -0
- package/rules/autoconsent/texas-gov.json +22 -0
- package/rules/autoconsent/tiresize.com.json +17 -0
- package/rules/autoconsent/tri-table.json +6 -2
- package/rules/autoconsent/trillian.json +10 -0
- package/rules/autoconsent/tyreseeker.json +44 -0
- package/rules/autoconsent/uhcprovider.json +13 -0
- package/rules/autoconsent/united-online.json +66 -0
- package/rules/autoconsent/unitedutilities.json +52 -0
- package/rules/autoconsent/uttings.co.uk.json +22 -0
- package/rules/autoconsent/vinnova-se.json +50 -0
- package/rules/autoconsent/vortex-consent.json +33 -0
- package/rules/autoconsent/watchfinder.json +16 -0
- package/rules/compact-rules.json +1 -1
- package/rules/rule-index.json +1 -1
- package/rules/rules.json +1 -1
- package/tests/1pmobile.spec.ts +5 -0
- package/tests/amasty-gdpr-hyva.spec.ts +17 -0
- package/tests/amazonforum.spec.ts +9 -0
- package/tests/asrock.spec.ts +3 -0
- package/tests/babylist.spec.ts +3 -0
- package/tests/babylonbee.spec.ts +6 -0
- package/tests/bathnes-cookie-banner.spec.ts +3 -0
- package/tests/chairish.spec.ts +3 -0
- package/tests/chemicalbook.spec.ts +5 -0
- package/tests/chimnie-co-uk.spec.ts +3 -0
- package/tests/complianz-optin.spec.ts +2 -0
- package/tests/consentmo.spec.ts +2 -0
- package/tests/cookie-pref-form.spec.ts +8 -0
- package/tests/cookie-script.spec.ts +5 -0
- package/tests/endeavorhealth.spec.ts +3 -0
- package/tests/ensighten.spec.ts +7 -1
- package/tests/epaas.spec.ts +3 -0
- package/tests/fepblue-org.spec.ts +3 -0
- package/tests/fnsp-cookieconsent.spec.ts +9 -0
- package/tests/fod-fujitv.spec.ts +3 -0
- package/tests/fpl-com.spec.ts +3 -0
- package/tests/fuqer.spec.ts +3 -0
- package/tests/greatschools.spec.ts +3 -0
- package/tests/hamradio-com.spec.ts +3 -0
- package/tests/hkdir.spec.ts +10 -0
- package/tests/hubspot.spec.ts +6 -1
- package/tests/ibkr.spec.ts +18 -0
- package/tests/jetpens.spec.ts +5 -0
- package/tests/jokul-cookie-consent.spec.ts +3 -0
- package/tests/kanopy.spec.ts +5 -0
- package/tests/mainlinemenswear.spec.ts +3 -0
- package/tests/mars-cookies-prompt.spec.ts +3 -0
- package/tests/money-co-uk.spec.ts +3 -0
- package/tests/myjobquote.spec.ts +5 -0
- package/tests/niedziela-pl.spec.ts +3 -0
- package/tests/nikkeibp.spec.ts +8 -0
- package/tests/northwell-edu.spec.ts +3 -0
- package/tests/nts-org-uk.spec.ts +3 -0
- package/tests/nyu-langone.spec.ts +3 -0
- package/tests/ofsted-reports-cosmetic.spec.ts +3 -0
- package/tests/onecom-cookie-banner.spec.ts +5 -0
- package/tests/optum-legal-banner.spec.ts +9 -0
- package/tests/optum.spec.ts +6 -0
- package/tests/oslosportslager.no.spec.ts +3 -0
- package/tests/planned-parenthood.spec.ts +3 -0
- package/tests/plex.tv.spec.ts +5 -0
- package/tests/pornhub.spec.ts +1 -1
- package/tests/radio-canada.spec.ts +15 -0
- package/tests/regjeringen.no.spec.ts +3 -0
- package/tests/reviewsolicitors.spec.ts +3 -0
- package/tests/roll20.spec.ts +3 -0
- package/tests/sankei-digital.spec.ts +9 -0
- package/tests/sirdata.spec.ts +13 -1
- package/tests/site-notifications-cookie-usage.spec.ts +3 -0
- package/tests/sparebank1-optin.spec.ts +8 -0
- package/tests/texas-gov.spec.ts +3 -0
- package/tests/tiresize.com.spec.ts +3 -0
- package/tests/tri-table.spec.ts +1 -1
- package/tests/trillian.spec.ts +15 -0
- package/tests/tyreseeker.spec.ts +3 -0
- package/tests/uhcprovider.spec.ts +3 -0
- package/tests/united-online.spec.ts +4 -0
- package/tests/unitedutilities.spec.ts +11 -0
- package/tests/uttings.co.uk.spec.ts +3 -0
- package/tests/vinnova-se.spec.ts +6 -0
- package/tests/vortex-consent.spec.ts +18 -0
- package/tests/watchfinder.spec.ts +3 -0
- package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
- package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
- package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
- package/tests-wtr/heuristics/heuristics-utils.test.ts +34 -4
- package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
- package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
- package/rules/generated/auto_US_interactivebrokers.com_0.json +0 -28
- package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
- package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
- package/tests/generated/auto_US_interactivebrokers.com_0.spec.ts +0 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect } from '@esm-bundle/chai';
|
|
2
|
-
import { getActionablePopups } from '../../lib/heuristics';
|
|
2
|
+
import { getActionablePopups, getPotentialPopups } from '../../lib/heuristics';
|
|
3
3
|
import { ButtonData } from '../../lib/types';
|
|
4
4
|
|
|
5
5
|
function rejectButtons(buttons: ButtonData[]) {
|
|
@@ -81,14 +81,30 @@ describe('getActionablePopups', () => {
|
|
|
81
81
|
expect(popups[0].regexClassification).to.equal('tier2');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
it('prefers a contentful fixed child over its contentful container', () => {
|
|
84
|
+
it('prefers a contentful fixed child over its contentful container in the DOM walk', () => {
|
|
85
|
+
showPopup('popup-with-contentful-fixed-child');
|
|
86
|
+
|
|
87
|
+
const popups = getPotentialPopups();
|
|
88
|
+
|
|
89
|
+
expect(popups.map((p) => p.element.id)).to.deep.equal(['contentful-fixed-child']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('acts on a contentful fixed child found by the DOM walk', () => {
|
|
85
93
|
showPopup('popup-with-contentful-fixed-child');
|
|
86
94
|
|
|
87
95
|
const popups = getActionablePopups('tier2');
|
|
88
96
|
|
|
89
|
-
expect(popups).to.
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
expect(popups.map((p) => [p.element.id, p.regexClassification])).to.deep.equal([['contentful-fixed-child', 'tier2']]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('prefers a container with reject found by hit-testing over its fixed child', () => {
|
|
101
|
+
showPopup('popup-with-contentful-fixed-child');
|
|
102
|
+
|
|
103
|
+
const popups = getActionablePopups('tier2', undefined, true);
|
|
104
|
+
|
|
105
|
+
expect(popups.map((p) => [p.element.id, p.regexClassification])).to.deep.equal([
|
|
106
|
+
['popup-with-contentful-fixed-child', 'reject'],
|
|
107
|
+
]);
|
|
92
108
|
});
|
|
93
109
|
|
|
94
110
|
it('finds acknowledge-only popup with no reject buttons', () => {
|
|
@@ -167,7 +183,7 @@ describe('getActionablePopups', () => {
|
|
|
167
183
|
expect(popups.length).to.equal(1);
|
|
168
184
|
});
|
|
169
185
|
|
|
170
|
-
it('ignores static positioned elements', () => {
|
|
186
|
+
it('ignores static positioned elements without hit-testing', () => {
|
|
171
187
|
showPopup('popup-static');
|
|
172
188
|
|
|
173
189
|
const popups = getActionablePopups();
|
|
@@ -175,6 +191,14 @@ describe('getActionablePopups', () => {
|
|
|
175
191
|
expect(popups.length).to.equal(0);
|
|
176
192
|
});
|
|
177
193
|
|
|
194
|
+
it('finds a static positioned popup on screen by hit-testing', () => {
|
|
195
|
+
showPopup('popup-static');
|
|
196
|
+
|
|
197
|
+
const popups = getActionablePopups('reject', undefined, true);
|
|
198
|
+
|
|
199
|
+
expect(popups.map((p) => p.element.id)).to.deep.equal(['popup-static']);
|
|
200
|
+
});
|
|
201
|
+
|
|
178
202
|
it('finds popup with role="dialog"', () => {
|
|
179
203
|
showPopup('popup-role-dialog');
|
|
180
204
|
|
|
@@ -202,7 +226,7 @@ describe('getActionablePopups', () => {
|
|
|
202
226
|
dialog.close();
|
|
203
227
|
});
|
|
204
228
|
|
|
205
|
-
it('ignores closed <dialog> element', () => {
|
|
229
|
+
it('ignores closed <dialog> element without hit-testing', () => {
|
|
206
230
|
const dialog = document.getElementById('popup-dialog-element') as HTMLDialogElement;
|
|
207
231
|
dialog.classList.add('visible');
|
|
208
232
|
|
|
@@ -210,6 +234,16 @@ describe('getActionablePopups', () => {
|
|
|
210
234
|
|
|
211
235
|
expect(popups.length).to.equal(0);
|
|
212
236
|
});
|
|
237
|
+
|
|
238
|
+
it('finds a closed <dialog> that the page shows anyway only by hit-testing', () => {
|
|
239
|
+
const dialog = document.getElementById('popup-dialog-element') as HTMLDialogElement;
|
|
240
|
+
dialog.classList.add('visible');
|
|
241
|
+
|
|
242
|
+
const popups = getActionablePopups('reject', undefined, true);
|
|
243
|
+
|
|
244
|
+
// not dialog-like while closed, but it is painted on screen
|
|
245
|
+
expect(popups.map((p) => p.element.id)).to.deep.equal(['popup-dialog-element']);
|
|
246
|
+
});
|
|
213
247
|
});
|
|
214
248
|
|
|
215
249
|
describe('button classification', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expect } from '@esm-bundle/chai';
|
|
2
2
|
import { getActionablePopups, getButtonData, getFramePopupElement, getPotentialPopups } from '../../lib/heuristics';
|
|
3
|
+
import { PopupData } from '../../lib/types';
|
|
3
4
|
import { getHidingStyle } from '../../lib/utils';
|
|
4
5
|
|
|
5
6
|
const BANNER = `
|
|
@@ -44,14 +45,18 @@ describe('getPotentialPopups', () => {
|
|
|
44
45
|
return attachShadow(container.querySelector('#host')!, html);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function popupIds(): string[] {
|
|
48
|
-
return getPotentialPopups(1000).map((p) => p.element.id);
|
|
48
|
+
function popupIds(hitTest = false): string[] {
|
|
49
|
+
return getPotentialPopups(1000, hitTest).map((p) => p.element.id);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function findPopup(id: string) {
|
|
52
53
|
return getPotentialPopups(1000).find((p) => p.element.id === id);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
function inContainer(popups: PopupData[]): PopupData[] {
|
|
57
|
+
return popups.filter((p) => container.contains(p.element));
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
it('returns positioned popups with their buttons', () => {
|
|
56
61
|
container.innerHTML = fixedBanner('popup');
|
|
57
62
|
const popup = findPopup('popup');
|
|
@@ -240,7 +245,7 @@ describe('getPotentialPopups', () => {
|
|
|
240
245
|
<div style="position: fixed; bottom: 0;"><button>Reject all</button><button>Accept all</button></div>
|
|
241
246
|
</div>
|
|
242
247
|
`;
|
|
243
|
-
const popups = getPotentialPopups(1000)
|
|
248
|
+
const popups = inContainer(getPotentialPopups(1000));
|
|
244
249
|
expect(popups.map((p) => p.element.id)).to.deep.equal(['banner']);
|
|
245
250
|
});
|
|
246
251
|
|
|
@@ -298,10 +303,236 @@ describe('getPotentialPopups', () => {
|
|
|
298
303
|
<div style="position: sticky; bottom: 0;"><button>Accept all</button></div>
|
|
299
304
|
</div>
|
|
300
305
|
`;
|
|
301
|
-
const popups = getActionablePopups('tier2', 1000)
|
|
306
|
+
const popups = inContainer(getActionablePopups('tier2', 1000));
|
|
302
307
|
expect(popups).to.have.length(0);
|
|
303
308
|
});
|
|
304
309
|
});
|
|
310
|
+
describe('banners in the normal page flow', () => {
|
|
311
|
+
const IN_FLOW_BANNER = `
|
|
312
|
+
<div id="banner">
|
|
313
|
+
<p>We use cookies to improve your experience. Read our <a href="#">Privacy Policy</a>.</p>
|
|
314
|
+
<button>Reject all</button>
|
|
315
|
+
<button>Accept all</button>
|
|
316
|
+
</div>
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
it('only hit-tests when asked to', () => {
|
|
320
|
+
container.innerHTML = IN_FLOW_BANNER;
|
|
321
|
+
expect(popupIds()).not.to.include('banner');
|
|
322
|
+
expect(inContainer(getActionablePopups('reject', 1000))).to.have.length(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('finds a banner at the top of the page that is not positioned', () => {
|
|
326
|
+
container.innerHTML = IN_FLOW_BANNER;
|
|
327
|
+
expect(popupIds(true)).to.include('banner');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('makes an in-flow banner actionable', () => {
|
|
331
|
+
container.innerHTML = IN_FLOW_BANNER;
|
|
332
|
+
const popup = getActionablePopups('reject', 1000, true).find((p) => p.element.id === 'banner');
|
|
333
|
+
expect(popup).to.exist;
|
|
334
|
+
expect(popup!.regexClassification).to.equal('reject');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('ignores an in-flow banner below the fold', () => {
|
|
338
|
+
container.innerHTML = `<div style="height: 3000px"></div>${IN_FLOW_BANNER}`;
|
|
339
|
+
expect(popupIds(true)).not.to.include('banner');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('ignores page copy about cookies without consent buttons', () => {
|
|
343
|
+
container.innerHTML =
|
|
344
|
+
'<div id="copy"><p>We use cookies on this site.</p><a href="#">Privacy Policy</a> <a href="#">Contact</a></div>';
|
|
345
|
+
expect(popupIds(true)).not.to.include('copy');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('ignores a login form with consent wording', () => {
|
|
349
|
+
container.innerHTML = `
|
|
350
|
+
<form id="login">
|
|
351
|
+
<input type="email" placeholder="Email">
|
|
352
|
+
<p>By continuing, you agree to our Terms and acknowledge our Privacy Policy and use of cookies.</p>
|
|
353
|
+
<button>Continue</button>
|
|
354
|
+
</form>
|
|
355
|
+
`;
|
|
356
|
+
expect(popupIds(true)).not.to.include('login');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('does not report a positioned popup twice', () => {
|
|
360
|
+
container.innerHTML = fixedBanner('banner');
|
|
361
|
+
expect(popupIds(true).filter((id) => id === 'banner')).to.have.length(1);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('prefers a positioned popup with reject over an in-flow accept-only banner', () => {
|
|
365
|
+
container.innerHTML = `
|
|
366
|
+
<div id="inflow"><p>We use cookies to improve your experience.</p><button>Accept all</button></div>
|
|
367
|
+
<div id="fixed" style="position: fixed; bottom: 0;"><p>We use cookies.</p><button>Reject all</button><button>Accept all</button></div>
|
|
368
|
+
`;
|
|
369
|
+
const popups = inContainer(getActionablePopups('tier2', 1000, true));
|
|
370
|
+
expect(popups.map((p) => [p.element.id, p.regexClassification])).to.deep.equal([
|
|
371
|
+
['fixed', 'reject'],
|
|
372
|
+
['inflow', 'tier2'],
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('skips the DOM walk when hit-testing finds a popup with reject', () => {
|
|
377
|
+
// pointer-events: none keeps the fixed popup out of hit-testing, so only the walk could find it
|
|
378
|
+
container.innerHTML = `
|
|
379
|
+
${IN_FLOW_BANNER}
|
|
380
|
+
<div id="fixed" style="position: fixed; bottom: 0; pointer-events: none;"><p>We use cookies.</p><button>Reject all</button></div>
|
|
381
|
+
`;
|
|
382
|
+
const actionable = inContainer(getActionablePopups('tier2', 1000, true));
|
|
383
|
+
expect(actionable.map((p) => p.element.id)).to.deep.equal(['banner']);
|
|
384
|
+
expect(popupIds(true)).to.include.members(['banner', 'fixed']);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('finds an in-flow banner inside a shadow root', () => {
|
|
388
|
+
shadowHost('', IN_FLOW_BANNER);
|
|
389
|
+
expect(popupIds(true)).to.include('banner');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('inside a walk candidate that is not a consent popup', () => {
|
|
393
|
+
// the fixed wrapper is found by the walk, but its age gate text excludes it
|
|
394
|
+
function ageGateWrapper(bannerFirst: boolean): string {
|
|
395
|
+
const banner =
|
|
396
|
+
'<div id="banner"><p>We use cookies to improve your experience on our website.</p><button>Accept all</button></div>';
|
|
397
|
+
const ageGate = '<p>You must be over 18 years old to enter.</p>';
|
|
398
|
+
return `<div id="wrapper" style="position: fixed; top: 0; left: 0; right: 0;">${bannerFirst ? banner + ageGate : ageGate + banner}</div>`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
it('keeps a consent popup found by hit-testing next to the walk candidate', () => {
|
|
402
|
+
// the top grid row hits the banner, so hit-testing finds the banner rather than the wrapper
|
|
403
|
+
container.innerHTML = ageGateWrapper(true);
|
|
404
|
+
const popups = inContainer(getPotentialPopups(1000, true));
|
|
405
|
+
expect(popups.map((p) => [p.element.id, p.source])).to.deep.equal([
|
|
406
|
+
['wrapper', 'walk'],
|
|
407
|
+
['banner', 'hitTest'],
|
|
408
|
+
]);
|
|
409
|
+
expect(popups[0].hitPoints).to.be.undefined;
|
|
410
|
+
expect(popups[1].hitPoints).not.to.be.empty;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('makes the consent popup found by hit-testing actionable', () => {
|
|
414
|
+
container.innerHTML = ageGateWrapper(true);
|
|
415
|
+
const actionable = inContainer(getActionablePopups('tier2', 1000, true));
|
|
416
|
+
expect(actionable.map((p) => [p.element.id, p.regexClassification])).to.deep.equal([['banner', 'tier2']]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('reports the wrapper once when hit-testing also reaches it', () => {
|
|
420
|
+
// the top grid row hits the age gate text, so the climb reaches the wrapper
|
|
421
|
+
container.innerHTML = ageGateWrapper(false);
|
|
422
|
+
const popups = inContainer(getPotentialPopups(1000, true));
|
|
423
|
+
expect(popups.map((p) => [p.element.id, p.source])).to.deep.equal([['wrapper', 'walk']]);
|
|
424
|
+
expect(popups[0].hitPoints).not.to.be.empty;
|
|
425
|
+
expect(inContainer(getActionablePopups('tier2', 1000, true))).to.be.empty;
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('keeps a banner with reject found by hit-testing next to its accept-only fragment found by the walk', () => {
|
|
430
|
+
// the walk keeps the fixed bar, an accept-only fragment that passes on its own; hit-testing the reject button reaches the banner
|
|
431
|
+
container.innerHTML = `
|
|
432
|
+
<div id="banner" style="position: fixed; bottom: 0; left: 0; right: 0; height: 200px;">
|
|
433
|
+
<div id="bar" style="position: fixed; bottom: 120px; left: 0; right: 0; height: 40px;"><span>We use cookies to improve your experience.</span><button>Accept all</button></div>
|
|
434
|
+
<button style="position: absolute; bottom: 0; left: 0; width: 100%; height: 100px;">Reject all</button>
|
|
435
|
+
</div>
|
|
436
|
+
`;
|
|
437
|
+
const popups = inContainer(getPotentialPopups(1000, true));
|
|
438
|
+
expect(popups.map((p) => [p.element.id, p.source, p.regexClassification])).to.deep.equal([
|
|
439
|
+
['bar', 'walk', 'tier2'],
|
|
440
|
+
['banner', 'hitTest', 'reject'],
|
|
441
|
+
]);
|
|
442
|
+
const actionable = inContainer(getActionablePopups('tier2', 1000, true));
|
|
443
|
+
expect(actionable.map((p) => p.element.id)).to.deep.equal(['banner']);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('order', () => {
|
|
448
|
+
it('sorts actionable popups by classification, and keeps search order within one', () => {
|
|
449
|
+
container.innerHTML = `
|
|
450
|
+
<div id="accept1" style="position: fixed; top: 0;"><p>We use cookies on this website.</p><button>Accept all</button></div>
|
|
451
|
+
<div id="ok" style="position: fixed; top: 100px;"><p>We use cookies on this website.</p><button>OK</button></div>
|
|
452
|
+
<div id="accept2" style="position: fixed; top: 200px;"><p>We use cookies on this website.</p><button>Accept all</button></div>
|
|
453
|
+
`;
|
|
454
|
+
const searchOrder = inContainer(getPotentialPopups(1000))
|
|
455
|
+
.filter((p) => p.regexClassification === 'tier2')
|
|
456
|
+
.map((p) => p.element.id);
|
|
457
|
+
expect(searchOrder).to.have.members(['accept1', 'accept2']);
|
|
458
|
+
const popups = inContainer(getActionablePopups('tier2', 1000));
|
|
459
|
+
expect(popups.map((p) => p.element.id)).to.deep.equal(['ok', ...searchOrder]);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('labels', () => {
|
|
464
|
+
it('labels every potential popup with a classification from its buttons', () => {
|
|
465
|
+
container.innerHTML = fixedBanner('consent');
|
|
466
|
+
// outside the container, so that the walk does not expand it to the container that holds the consent banner
|
|
467
|
+
const chat = document.createElement('div');
|
|
468
|
+
chat.innerHTML = '<div id="chat" style="position: fixed; top: 0;"><p>Chat with us</p><button>Start chat</button></div>';
|
|
469
|
+
document.body.appendChild(chat);
|
|
470
|
+
try {
|
|
471
|
+
const labels = Object.fromEntries(
|
|
472
|
+
getPotentialPopups(1000)
|
|
473
|
+
.filter((p) => container.contains(p.element) || chat.contains(p.element))
|
|
474
|
+
.map((p) => [p.element.id, p.regexClassification]),
|
|
475
|
+
);
|
|
476
|
+
expect(labels).to.deep.equal({ consent: 'reject', chat: 'none' });
|
|
477
|
+
} finally {
|
|
478
|
+
chat.remove();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('hit-test grid and hit points', () => {
|
|
484
|
+
const GRID = { columns: [0.25, 0.75], edgeRows: [40] };
|
|
485
|
+
const columnXs = GRID.columns.map((c) => Math.round(c * window.innerWidth));
|
|
486
|
+
|
|
487
|
+
function findGridPopup(id: string) {
|
|
488
|
+
return getPotentialPopups(1000, true, GRID).find((p) => p.element.id === id);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
it('records every grid point that reached an in-flow banner', () => {
|
|
492
|
+
container.innerHTML = `<div id="banner" style="padding: 60px 0;">${BANNER}</div>`;
|
|
493
|
+
const popup = findGridPopup('banner');
|
|
494
|
+
expect(popup?.source).to.equal('hitTest');
|
|
495
|
+
expect(popup!.hitPoints).to.deep.equal(columnXs.map((x) => [x, 40]));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('keeps the walk candidate of a positioned popup, with its hit points', () => {
|
|
499
|
+
container.innerHTML = `<div id="fixed" style="position: fixed; bottom: 0; left: 0; right: 0; padding: 30px 0;">${BANNER}</div>`;
|
|
500
|
+
const popup = findGridPopup('fixed');
|
|
501
|
+
expect(popup?.source).to.equal('walk');
|
|
502
|
+
const bottomRow = Math.round(window.innerHeight - 40);
|
|
503
|
+
expect(popup!.hitPoints).to.deep.equal(columnXs.map((x) => [x, bottomRow]));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('leaves out hit points of a positioned popup that is not hit-tested', () => {
|
|
507
|
+
container.innerHTML = fixedBanner('fixed');
|
|
508
|
+
const popup = getPotentialPopups(1000).find((p) => p.element.id === 'fixed');
|
|
509
|
+
expect(popup?.source).to.equal('walk');
|
|
510
|
+
expect(popup!.hitPoints).to.be.undefined;
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('tests rows 30 and 90px from the edges by default', () => {
|
|
514
|
+
// the banner lies between 100 and 200px from the top; the spacer is outside the container, so climbs from it stop at body
|
|
515
|
+
const spacer = document.createElement('div');
|
|
516
|
+
spacer.style.height = '100px';
|
|
517
|
+
document.body.insertBefore(spacer, document.body.firstChild);
|
|
518
|
+
document.body.style.margin = '0';
|
|
519
|
+
container.innerHTML = `<div id="banner" style="height: 100px; overflow: hidden;">${BANNER}</div>`;
|
|
520
|
+
try {
|
|
521
|
+
expect(container.querySelector('#banner')!.getBoundingClientRect().top).to.equal(100);
|
|
522
|
+
expect(inContainer(getPotentialPopups(1000, true))).to.have.length(0);
|
|
523
|
+
const withRow150 = getPotentialPopups(1000, true, { columns: [0.1, 0.5, 0.9], edgeRows: [30, 90, 150] });
|
|
524
|
+
expect(withRow150.map((p) => p.element.id)).to.include('banner');
|
|
525
|
+
} finally {
|
|
526
|
+
spacer.remove();
|
|
527
|
+
document.body.style.margin = '';
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('only tests the points of the grid it is given', () => {
|
|
532
|
+
container.innerHTML = `<div id="banner" style="padding: 60px 0;">${BANNER}</div>`;
|
|
533
|
+
expect(getPotentialPopups(1000, true, { columns: [0.5], edgeRows: [] }).map((p) => p.element.id)).not.to.include('banner');
|
|
534
|
+
});
|
|
535
|
+
});
|
|
305
536
|
});
|
|
306
537
|
|
|
307
538
|
describe('getFramePopupElement', () => {
|
|
@@ -5,11 +5,12 @@ import { HeuristicLevel } from '../../lib/types';
|
|
|
5
5
|
|
|
6
6
|
// must be run from heuristic-cmp.html
|
|
7
7
|
describe('AutoConsentHeuristicCMP', () => {
|
|
8
|
-
function createAutoconsent(heuristicMode: HeuristicLevel) {
|
|
8
|
+
function createAutoconsent(heuristicMode: HeuristicLevel, enableHeuristicHitTest = false) {
|
|
9
9
|
return new Autoconsent(() => Promise.resolve(), {
|
|
10
10
|
enabled: false,
|
|
11
11
|
autoAction: null,
|
|
12
12
|
heuristicMode,
|
|
13
|
+
enableHeuristicHitTest,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -40,6 +41,20 @@ describe('AutoConsentHeuristicCMP', () => {
|
|
|
40
41
|
expect(cmp.name).to.equal('HEURISTIC-REJECT');
|
|
41
42
|
});
|
|
42
43
|
|
|
44
|
+
it('hit-tests for an in-flow banner only when enableHeuristicHitTest is set', async () => {
|
|
45
|
+
const banner = document.createElement('div');
|
|
46
|
+
banner.innerHTML = '<p>We use cookies to improve your experience.</p><button>Reject all</button><button>Accept all</button>';
|
|
47
|
+
document.body.prepend(banner);
|
|
48
|
+
try {
|
|
49
|
+
expect(await new AutoConsentHeuristicCMP(createAutoconsent('tier2'), 'tier2').detectCmp()).to.be.false;
|
|
50
|
+
const cmp = new AutoConsentHeuristicCMP(createAutoconsent('tier2', true), 'tier2');
|
|
51
|
+
expect(await cmp.detectCmp()).to.be.true;
|
|
52
|
+
expect(cmp.name).to.equal('HEURISTIC-REJECT');
|
|
53
|
+
} finally {
|
|
54
|
+
banner.remove();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
43
58
|
it('detects a Russian popup using the Cyrillic word for cookies', async () => {
|
|
44
59
|
const autoconsent = createAutoconsent('tier2');
|
|
45
60
|
const cmp = new AutoConsentHeuristicCMP(autoconsent, 'tier2');
|
|
@@ -75,7 +75,7 @@ describe('checkHeuristicPatterns', () => {
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
describe('checkHeuristicPatterns with
|
|
78
|
+
describe('checkHeuristicPatterns with qualifiers before "cookies"', () => {
|
|
79
79
|
it('detects "we use <qualifier> cookies" notices', () => {
|
|
80
80
|
const texts = [
|
|
81
81
|
// openrouter.ai
|
|
@@ -83,6 +83,8 @@ describe('checkHeuristicPatterns with a qualifier before "cookies"', () => {
|
|
|
83
83
|
'We use optional cookies to measure traffic',
|
|
84
84
|
'We use analytics cookies and similar technologies',
|
|
85
85
|
'We serve necessary cookies only',
|
|
86
|
+
'We use Google Analytics cookies to understand which pages and tools are useful.',
|
|
87
|
+
'We use third party cookies for advertising',
|
|
86
88
|
];
|
|
87
89
|
for (const text of texts) {
|
|
88
90
|
expect(checkHeuristicPatterns(text).patterns.length, text).to.be.greaterThan(0);
|
|
@@ -90,9 +92,9 @@ describe('checkHeuristicPatterns with a qualifier before "cookies"', () => {
|
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
it('does not match when "cookies" is further away', () => {
|
|
93
|
-
expect(
|
|
94
|
-
0,
|
|
95
|
-
);
|
|
95
|
+
expect(
|
|
96
|
+
checkHeuristicPatterns('We use the best chocolate in our cookies', [/we (?:use|serve)(?: \w+){0,2} cookies/gi]).patterns,
|
|
97
|
+
).to.have.length(0);
|
|
96
98
|
});
|
|
97
99
|
});
|
|
98
100
|
|
|
@@ -195,6 +197,23 @@ describe('checkHeuristicPatterns with Danish popups', () => {
|
|
|
195
197
|
});
|
|
196
198
|
});
|
|
197
199
|
|
|
200
|
+
describe('checkHeuristicPatterns with Norwegian popups', () => {
|
|
201
|
+
it('detects Norwegian cookie notices', () => {
|
|
202
|
+
const texts = [
|
|
203
|
+
'Kan vi bruke valgfrie informasjonskapsler for å forbedre tjenestene våre?',
|
|
204
|
+
'Vi bruker informasjonskapsler for å gi deg en bedre opplevelse.',
|
|
205
|
+
'Nettstedet bruker cookies til statistikk.',
|
|
206
|
+
];
|
|
207
|
+
for (const text of texts) {
|
|
208
|
+
expect(checkHeuristicPatterns(text).patterns.length, text).to.be.greaterThan(0);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('does not detect unrelated Norwegian text', () => {
|
|
213
|
+
expect(checkHeuristicPatterns('Vi bruker skattemeldingen til å beregne skatten din').patterns).to.have.length(0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
198
217
|
describe('isExcludedPopup', () => {
|
|
199
218
|
it('flags doublelist-style age verification popups', () => {
|
|
200
219
|
// Real-world text from the doublelist.com age gate (listings page)
|
|
@@ -349,6 +368,17 @@ describe('classifyButtonTextRegex', () => {
|
|
|
349
368
|
expect(classifyButtonTextRegex('Læs mere om brugen af cookies på sundhed.dk')).not.to.equal('reject');
|
|
350
369
|
});
|
|
351
370
|
|
|
371
|
+
it('matches Norwegian reject buttons', () => {
|
|
372
|
+
expect(classifyButtonTextRegex('Nei')).to.equal('reject');
|
|
373
|
+
expect(classifyButtonTextRegex('Nei takk')).to.equal('reject');
|
|
374
|
+
expect(classifyButtonTextRegex('Avvis alle')).to.equal('reject');
|
|
375
|
+
expect(classifyButtonTextRegex('Avslå')).to.equal('reject');
|
|
376
|
+
expect(classifyButtonTextRegex('Godta kun nødvendige')).to.equal('reject');
|
|
377
|
+
expect(classifyButtonTextRegex('Bare nødvendige informasjonskapsler')).to.equal('reject');
|
|
378
|
+
expect(classifyButtonTextRegex('Ja')).not.to.equal('reject');
|
|
379
|
+
expect(classifyButtonTextRegex('Les mer om hvordan vi bruker informasjonskapsler')).not.to.equal('reject');
|
|
380
|
+
});
|
|
381
|
+
|
|
352
382
|
it('matches Russian settings buttons', () => {
|
|
353
383
|
expect(classifyButtonTextRegex('Настроить')).to.equal('settings');
|
|
354
384
|
expect(classifyButtonTextRegex('Настройки')).to.equal('settings');
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "auto_CA_plex.tv_ee4",
|
|
3
|
-
"cosmetic": false,
|
|
4
|
-
"_metadata": {
|
|
5
|
-
"vendorUrl": "http://www.plex.tv/"
|
|
6
|
-
},
|
|
7
|
-
"runContext": {
|
|
8
|
-
"main": true,
|
|
9
|
-
"frame": false,
|
|
10
|
-
"urlPattern": "^https?://(www\\.)?plex\\.tv/"
|
|
11
|
-
},
|
|
12
|
-
"prehideSelectors": [],
|
|
13
|
-
"detectCmp": [
|
|
14
|
-
{
|
|
15
|
-
"exists": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])"
|
|
16
|
-
}
|
|
17
|
-
],
|
|
18
|
-
"detectPopup": [
|
|
19
|
-
{
|
|
20
|
-
"visible": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])"
|
|
21
|
-
}
|
|
22
|
-
],
|
|
23
|
-
"optIn": [],
|
|
24
|
-
"optOut": [
|
|
25
|
-
{
|
|
26
|
-
"wait": 500
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"waitForThenClick": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])",
|
|
30
|
-
"comment": "I Reject"
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
|
-
"test": [
|
|
34
|
-
{
|
|
35
|
-
"waitForVisible": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])",
|
|
36
|
-
"timeout": 1000,
|
|
37
|
-
"check": "none"
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "auto_CA_support.plex.tv_gn3",
|
|
3
|
-
"cosmetic": false,
|
|
4
|
-
"_metadata": {
|
|
5
|
-
"vendorUrl": "http://support.plex.tv/"
|
|
6
|
-
},
|
|
7
|
-
"runContext": {
|
|
8
|
-
"main": true,
|
|
9
|
-
"frame": false,
|
|
10
|
-
"urlPattern": "^https?://(www\\.)?support\\.plex\\.tv/"
|
|
11
|
-
},
|
|
12
|
-
"prehideSelectors": [],
|
|
13
|
-
"detectCmp": [
|
|
14
|
-
{
|
|
15
|
-
"exists": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])"
|
|
16
|
-
}
|
|
17
|
-
],
|
|
18
|
-
"detectPopup": [
|
|
19
|
-
{
|
|
20
|
-
"visible": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])"
|
|
21
|
-
}
|
|
22
|
-
],
|
|
23
|
-
"optIn": [],
|
|
24
|
-
"optOut": [
|
|
25
|
-
{
|
|
26
|
-
"wait": 500
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"waitForThenClick": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])",
|
|
30
|
-
"comment": "I Reject"
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
|
-
"test": [
|
|
34
|
-
{
|
|
35
|
-
"waitForVisible": "body > div:not([id]) > div:nth-child(3):not([id]) > div:nth-child(1):not([id]) > div:nth-child(2):not([id]) > a:not([id])",
|
|
36
|
-
"timeout": 1000,
|
|
37
|
-
"check": "none"
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "auto_US_interactivebrokers.com_0",
|
|
3
|
-
"vendorUrl": "https://www.interactivebrokers.com/",
|
|
4
|
-
"cosmetic": false,
|
|
5
|
-
"runContext": {
|
|
6
|
-
"main": true,
|
|
7
|
-
"frame": false,
|
|
8
|
-
"urlPattern": "^https?://(www\\.)?interactivebrokers\\.com/"
|
|
9
|
-
},
|
|
10
|
-
"prehideSelectors": [],
|
|
11
|
-
"detectCmp": [
|
|
12
|
-
{
|
|
13
|
-
"exists": "body > div[class][id][tabindex][role][aria-labelledby][aria-modal][style] > div > div > div:nth-child(3) > div > div > p > button:nth-child(3)"
|
|
14
|
-
}
|
|
15
|
-
],
|
|
16
|
-
"detectPopup": [
|
|
17
|
-
{
|
|
18
|
-
"visible": "body > div[class][id][tabindex][role][aria-labelledby][aria-modal][style] > div > div > div:nth-child(3) > div > div > p > button:nth-child(3)"
|
|
19
|
-
}
|
|
20
|
-
],
|
|
21
|
-
"optIn": [],
|
|
22
|
-
"optOut": [
|
|
23
|
-
{
|
|
24
|
-
"waitForThenClick": "body > div[class][id][tabindex][role][aria-labelledby][aria-modal][style] > div > div > div:nth-child(3) > div > div > p > button:nth-child(3)",
|
|
25
|
-
"text": "Reject All Cookies"
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|