@duckduckgo/autoconsent 16.63.0 → 17.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 (84) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/addon-firefox/background.bundle.js +1 -0
  3. package/dist/addon-firefox/compact-rules.json +1 -1
  4. package/dist/addon-firefox/content.bundle.js +162 -35
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-firefox/popup.bundle.js +1 -0
  7. package/dist/addon-firefox/rule-index.json +1 -1
  8. package/dist/addon-firefox/rules.json +1 -1
  9. package/dist/addon-mv3/background.bundle.js +1 -0
  10. package/dist/addon-mv3/compact-rules.json +1 -1
  11. package/dist/addon-mv3/content.bundle.js +162 -35
  12. package/dist/addon-mv3/manifest.json +1 -1
  13. package/dist/addon-mv3/popup.bundle.js +1 -0
  14. package/dist/addon-mv3/rule-index.json +1 -1
  15. package/dist/addon-mv3/rules.json +1 -1
  16. package/dist/autoconsent.cjs.js +162 -35
  17. package/dist/autoconsent.esm.js +162 -35
  18. package/dist/autoconsent.playwright.js +165 -35
  19. package/dist/autoconsent.standalone.js +163 -36
  20. package/dist/types/heuristics.d.ts +12 -3
  21. package/dist/types/types.d.ts +12 -1
  22. package/lib/cmps/base.ts +5 -1
  23. package/lib/heuristics.ts +240 -45
  24. package/lib/types.ts +16 -1
  25. package/lib/utils.ts +1 -0
  26. package/package.json +1 -1
  27. package/readme.md +1 -1
  28. package/rules/autoconsent/asrock.json +44 -0
  29. package/rules/autoconsent/consentmo.json +5 -2
  30. package/rules/autoconsent/cookie-script.json +34 -15
  31. package/rules/autoconsent/epaas.json +18 -1
  32. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  33. package/rules/autoconsent/fod-fujitv.json +13 -0
  34. package/rules/autoconsent/hkdir.json +33 -0
  35. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  36. package/rules/autoconsent/lego.json +27 -5
  37. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  38. package/rules/autoconsent/niedziela-pl.json +35 -0
  39. package/rules/autoconsent/nikkeibp.json +10 -0
  40. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  41. package/rules/autoconsent/oslosportslager.no.json +34 -0
  42. package/rules/autoconsent/plex.tv.json +52 -0
  43. package/rules/autoconsent/radio-canada.json +28 -0
  44. package/rules/autoconsent/regjeringen.no.json +13 -0
  45. package/rules/autoconsent/roll20.json +50 -0
  46. package/rules/autoconsent/sirdata.json +8 -3
  47. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  48. package/rules/autoconsent/sparebank1-optin.json +10 -0
  49. package/rules/autoconsent/tri-table.json +6 -2
  50. package/rules/autoconsent/vinnova-se.json +50 -0
  51. package/rules/autoconsent/vortex-consent.json +33 -0
  52. package/rules/compact-rules.json +1 -1
  53. package/rules/rule-index.json +1 -1
  54. package/rules/rules.json +1 -1
  55. package/tests/asrock.spec.ts +3 -0
  56. package/tests/consentmo.spec.ts +2 -0
  57. package/tests/cookie-script.spec.ts +5 -0
  58. package/tests/epaas.spec.ts +3 -0
  59. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  60. package/tests/fod-fujitv.spec.ts +3 -0
  61. package/tests/hkdir.spec.ts +10 -0
  62. package/tests/jokul-cookie-consent.spec.ts +3 -0
  63. package/tests/mars-cookies-prompt.spec.ts +3 -0
  64. package/tests/niedziela-pl.spec.ts +3 -0
  65. package/tests/nikkeibp.spec.ts +8 -0
  66. package/tests/onecom-cookie-banner.spec.ts +5 -0
  67. package/tests/oslosportslager.no.spec.ts +3 -0
  68. package/tests/plex.tv.spec.ts +5 -0
  69. package/tests/radio-canada.spec.ts +15 -0
  70. package/tests/regjeringen.no.spec.ts +3 -0
  71. package/tests/roll20.spec.ts +3 -0
  72. package/tests/sirdata.spec.ts +13 -1
  73. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  74. package/tests/sparebank1-optin.spec.ts +8 -0
  75. package/tests/tri-table.spec.ts +1 -1
  76. package/tests/vinnova-se.spec.ts +6 -0
  77. package/tests/vortex-consent.spec.ts +18 -0
  78. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  79. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  80. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  81. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  82. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  83. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  84. package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('asrock', ['https://www.asrock.com/', 'https://www.asrock.com/index.jp.asp']);
@@ -9,6 +9,8 @@ generateCMPTests(
9
9
  'https://eu.blackdiamondequipment.com/',
10
10
  'https://www.directdoors.com/',
11
11
  'https://goorin.com/',
12
+ 'https://nl.pit-pit.com/',
13
+ 'https://marshallsgarden.com/',
12
14
  ],
13
15
  {
14
16
  skipRegions: ['US'],
@@ -12,3 +12,8 @@ generateCMPTests('cookie-script', [
12
12
  'https://n8n.io/',
13
13
  'https://hearthis.at/',
14
14
  ]);
15
+
16
+ // category checkboxes in the banner, no manage button, hidden "Save & Close"; site is blocked outside Europe
17
+ generateCMPTests('cookie-script', ['https://alledaek365.dk/'], {
18
+ onlyRegions: ['DE', 'FR', 'NL', 'PL', 'DK'],
19
+ });
@@ -4,3 +4,6 @@ generateCMPTests('epaas', ['https://www.bmw.de/de/index.html', 'https://www.mini
4
4
 
5
5
  // No explicit reject button outside the EEA: opt-out goes through the "customise" layer.
6
6
  generateCMPTests('epaas', ['https://www.mini.jp/ja_JP/home.html'], { testOptIn: false });
7
+
8
+ // Opt-out-by-default variant: marketing pre-enabled in the "customise" layer, saved with "Continue".
9
+ generateCMPTests('epaas', ['https://www.bmw.com.au/en/index.html', 'https://www.mini.com.au/', 'https://www.bmw.co.nz/']);
@@ -0,0 +1,9 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('fnsp-cookieconsent', [
4
+ 'https://www.helse-bergen.no/',
5
+ 'https://www.helse-vest.no/',
6
+ 'https://www.oslo-universitetssykehus.no/',
7
+ 'https://www.stolav.no/',
8
+ 'https://www.ahus.no/',
9
+ ]);
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('fod-fujitv', ['https://fod.fujitv.co.jp/']);
@@ -0,0 +1,10 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ // the site reloads the page right after the click, so the self test cannot run
4
+ generateCMPTests(
5
+ 'hkdir',
6
+ ['https://hkdir.no/', 'https://studentombudfagskole.no/', 'https://fagskoleradet.no/', 'https://erihplus.hkdir.no/'],
7
+ {
8
+ testSelfTest: false,
9
+ },
10
+ );
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('jokul-cookie-consent', ['https://www.fremtind.no/', 'https://meldskade.fremtind.no/']);
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('mars-cookies-prompt', ['https://www.bergenkino.no/', 'https://www.trondheimkino.no/', 'https://www.kimenkino.no/']);
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('niedziela-pl', ['https://niezbednik.niedziela.pl/', 'https://krakow.niedziela.pl/', 'https://m.niedziela.pl/']);
@@ -0,0 +1,8 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('nikkeibp', [
4
+ 'https://natgeo.nikkeibp.co.jp/',
5
+ 'https://xtech.nikkei.com/',
6
+ 'https://business.nikkei.com/',
7
+ 'https://bio.nikkeibp.co.jp/',
8
+ ]);
@@ -0,0 +1,5 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('onecom-cookie-banner', ['http://utenfilter.no/', 'https://mtv-einheit.de/', 'https://oxarna.se/'], {
4
+ testSelfTest: false,
5
+ });
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('oslosportslager.no', ['https://oslosportslager.no/']);
@@ -0,0 +1,5 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('plex.tv', ['https://watch.plex.tv/', 'https://support.plex.tv/', 'https://www.plex.tv/en-gb/plans/'], {
4
+ skipRegions: ['US', 'CA', 'AU', 'JP'],
5
+ });
@@ -0,0 +1,15 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests(
4
+ 'radio-canada',
5
+ [
6
+ 'https://ici.radio-canada.ca/',
7
+ 'https://ici.radio-canada.ca/info',
8
+ 'https://ici.radio-canada.ca/ohdio',
9
+ 'https://ici.artv.ca/',
10
+ 'https://ici.exploratv.ca/',
11
+ ],
12
+ {
13
+ testSelfTest: false,
14
+ },
15
+ );
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('regjeringen.no', ['https://www.regjeringen.no/', 'https://www.regjeringen.no/en/id4/']);
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('roll20', ['https://roll20.net/', 'https://marketplace.roll20.net/', 'https://roll20.net/compendium/dnd5e']);
@@ -1,3 +1,15 @@
1
1
  import generateCMPTests from '../playwright/runner';
2
2
 
3
- generateCMPTests('Sirdata', ['https://www.comment-economiser.fr/', 'https://gizmodo.com/']);
3
+ // GDPR flavor: "Do not accept" button, or only a close (X) button
4
+ generateCMPTests(
5
+ 'Sirdata',
6
+ ['https://www.comment-economiser.fr/', 'https://www.futbolfantasy.com/', 'https://kotaku.com/', 'https://upway.de/'],
7
+ {
8
+ skipRegions: ['US'],
9
+ },
10
+ );
11
+
12
+ // US flavor: reject via "Set your choices"
13
+ generateCMPTests('Sirdata', ['https://www.medicaldaily.com/', 'https://www.ibtimes.co.uk/'], {
14
+ onlyRegions: ['US'],
15
+ });
@@ -0,0 +1,3 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('site-notifications-cookie-usage', ['https://www.mrskin.com/', 'https://www.mrman.com/']);
@@ -0,0 +1,8 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ generateCMPTests('sparebank1-optin', ['https://www.sparebank1.no/', 'https://www.sparebank1.no/nb/ostlandet/privat.html']);
4
+
5
+ // the page reloads right after opt-out, which interrupts the self-test
6
+ generateCMPTests('sparebank1-optin', ['https://www.eiendomsmegler1.no/'], {
7
+ testSelfTest: false,
8
+ });
@@ -1,5 +1,5 @@
1
1
  import generateCMPTests from '../playwright/runner';
2
2
 
3
- generateCMPTests('tri-table', ['https://ckm.pl/', 'https://tri-table.com/'], {
3
+ generateCMPTests('tri-table', ['https://ckm.pl/', 'https://tri-table.com/', 'https://wiadomosci.dziennik.pl/', 'https://forsal.pl/'], {
4
4
  skipRegions: ['US'],
5
5
  });
@@ -0,0 +1,6 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ // saving the choice reloads the page before the self-test can run
4
+ generateCMPTests('vinnova-se', ['https://www.vinnova.se/', 'https://www.vinnova.se/en/'], {
5
+ testSelfTest: false,
6
+ });
@@ -0,0 +1,18 @@
1
+ import generateCMPTests from '../playwright/runner';
2
+
3
+ // The page reloads after the reject click, so the self-test never runs
4
+ generateCMPTests(
5
+ 'vortex-consent',
6
+ [
7
+ 'https://www.inn.no/',
8
+ 'https://www.uio.no/',
9
+ 'https://www.hiof.no/',
10
+ 'https://www.nih.no/',
11
+ 'https://www.himolde.no/',
12
+ 'https://www.samordnaopptak.no/',
13
+ 'https://www.aho.no/',
14
+ ],
15
+ {
16
+ testSelfTest: false,
17
+ },
18
+ );
@@ -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.have.length(1);
90
- expect(popups[0].element.id).to.equal('contentful-fixed-child');
91
- expect(popups[0].regexClassification).to.equal('tier2');
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).filter((p) => container.contains(p.element));
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).filter((p) => container.contains(p.element));
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');
@@ -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
- }