@duckduckgo/autoconsent 16.62.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 (139) hide show
  1. package/CHANGELOG.md +84 -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 +176 -36
  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 +176 -36
  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 +176 -36
  17. package/dist/autoconsent.esm.js +176 -36
  18. package/dist/autoconsent.playwright.js +179 -36
  19. package/dist/autoconsent.standalone.js +177 -37
  20. package/dist/heuristics.cjs.js +14 -1
  21. package/dist/heuristics.mjs +14 -1
  22. package/dist/types/heuristics.d.ts +12 -3
  23. package/dist/types/types.d.ts +12 -1
  24. package/lib/cmps/base.ts +5 -1
  25. package/lib/heuristic-patterns.ts +17 -1
  26. package/lib/heuristics.ts +240 -45
  27. package/lib/types.ts +16 -1
  28. package/lib/utils.ts +1 -0
  29. package/package.json +2 -2
  30. package/readme.md +1 -1
  31. package/rules/autoconsent/appconsent-legacy.json +22 -1
  32. package/rules/autoconsent/asrock.json +44 -0
  33. package/rules/autoconsent/beetle-tracking.json +22 -0
  34. package/rules/autoconsent/casier-judiciaire-justice-gouv-fr.json +40 -0
  35. package/rules/autoconsent/civilservicejobs-service-gov-uk.json +38 -0
  36. package/rules/autoconsent/ckies.json +1 -1
  37. package/rules/autoconsent/consentmo.json +5 -2
  38. package/rules/autoconsent/cookie-script.json +34 -15
  39. package/rules/autoconsent/cookiesinfo-closecookies.json +9 -0
  40. package/rules/autoconsent/dack365.json +35 -0
  41. package/rules/autoconsent/epaas.json +18 -1
  42. package/rules/autoconsent/eu-cookie-law-popup.json +33 -0
  43. package/rules/autoconsent/fascicolosanitario-lombardia.json +14 -0
  44. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  45. package/rules/autoconsent/fod-fujitv.json +13 -0
  46. package/rules/autoconsent/fundingchoices.json +12 -2
  47. package/rules/autoconsent/hkdir.json +33 -0
  48. package/rules/autoconsent/hyrabostad.json +21 -0
  49. package/rules/autoconsent/insided.json +15 -0
  50. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  51. package/rules/autoconsent/kartverket.json +46 -0
  52. package/rules/autoconsent/lego.json +27 -5
  53. package/rules/autoconsent/luxmed.json +33 -0
  54. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  55. package/rules/autoconsent/meta-cookie-dialog.json +37 -0
  56. package/rules/autoconsent/mittanbud.json +33 -0
  57. package/rules/autoconsent/niedziela-pl.json +35 -0
  58. package/rules/autoconsent/nikkeibp.json +10 -0
  59. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  60. package/rules/autoconsent/oslosportslager.no.json +34 -0
  61. package/rules/autoconsent/plex.tv.json +52 -0
  62. package/rules/autoconsent/poki.json +37 -0
  63. package/rules/autoconsent/polisen-se.json +26 -0
  64. package/rules/autoconsent/radio-canada.json +28 -0
  65. package/rules/autoconsent/regjeringen.no.json +13 -0
  66. package/rules/autoconsent/roll20.json +50 -0
  67. package/rules/autoconsent/romeo.json +47 -0
  68. package/rules/autoconsent/sirdata.json +8 -3
  69. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  70. package/rules/autoconsent/sparebank1-optin.json +10 -0
  71. package/rules/autoconsent/stockholm-stad.json +42 -0
  72. package/rules/autoconsent/stright.json +19 -1
  73. package/rules/autoconsent/stripchat.com.json +26 -5
  74. package/rules/autoconsent/tri-table.json +6 -2
  75. package/rules/autoconsent/tvp.json +27 -0
  76. package/rules/autoconsent/urvaerket.json +33 -0
  77. package/rules/autoconsent/vinnova-se.json +50 -0
  78. package/rules/autoconsent/volvocars-com.json +23 -2
  79. package/rules/autoconsent/vortex-consent.json +33 -0
  80. package/rules/autoconsent/vue-cookie-accept-decline.json +42 -0
  81. package/rules/compact-rules.json +1 -1
  82. package/rules/rule-index.json +1 -1
  83. package/rules/rules.json +1 -1
  84. package/tests/appconsent-legacy.spec.ts +1 -1
  85. package/tests/asrock.spec.ts +3 -0
  86. package/tests/beetle-tracking.spec.ts +3 -0
  87. package/tests/casier-judiciaire-justice-gouv-fr.spec.ts +3 -0
  88. package/tests/civilservicejobs-service-gov-uk.spec.ts +3 -0
  89. package/tests/consentmo.spec.ts +2 -0
  90. package/tests/cookie-script.spec.ts +5 -0
  91. package/tests/cookiesinfo-closecookies.spec.ts +8 -0
  92. package/tests/dack365.spec.ts +3 -0
  93. package/tests/epaas.spec.ts +3 -0
  94. package/tests/eu-cookie-law-popup.spec.ts +8 -0
  95. package/tests/fascicolosanitario-lombardia.spec.ts +3 -0
  96. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  97. package/tests/fod-fujitv.spec.ts +3 -0
  98. package/tests/fundingchoices.spec.ts +7 -1
  99. package/tests/hkdir.spec.ts +10 -0
  100. package/tests/hyrabostad.spec.ts +3 -0
  101. package/tests/insided.spec.ts +9 -0
  102. package/tests/jokul-cookie-consent.spec.ts +3 -0
  103. package/tests/kartverket.spec.ts +3 -0
  104. package/tests/luxmed.spec.ts +3 -0
  105. package/tests/mars-cookies-prompt.spec.ts +3 -0
  106. package/tests/meta-cookie-dialog.spec.ts +14 -0
  107. package/tests/mittanbud.spec.ts +3 -0
  108. package/tests/niedziela-pl.spec.ts +3 -0
  109. package/tests/nikkeibp.spec.ts +8 -0
  110. package/tests/onecom-cookie-banner.spec.ts +5 -0
  111. package/tests/oslosportslager.no.spec.ts +3 -0
  112. package/tests/plex.tv.spec.ts +5 -0
  113. package/tests/poki.spec.ts +6 -0
  114. package/tests/polisen-se.spec.ts +3 -0
  115. package/tests/radio-canada.spec.ts +15 -0
  116. package/tests/regjeringen.no.spec.ts +3 -0
  117. package/tests/roll20.spec.ts +3 -0
  118. package/tests/romeo.spec.ts +3 -0
  119. package/tests/sirdata.spec.ts +13 -1
  120. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  121. package/tests/sparebank1-optin.spec.ts +8 -0
  122. package/tests/stockholm-stad.spec.ts +7 -0
  123. package/tests/stright.spec.ts +11 -0
  124. package/tests/stripchat.com.spec.ts +4 -1
  125. package/tests/tri-table.spec.ts +1 -1
  126. package/tests/tvp.spec.ts +3 -0
  127. package/tests/urvaerket.spec.ts +13 -0
  128. package/tests/vinnova-se.spec.ts +6 -0
  129. package/tests/volvocars-com.spec.ts +1 -1
  130. package/tests/vortex-consent.spec.ts +18 -0
  131. package/tests/vue-cookie-accept-decline.spec.ts +8 -0
  132. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  133. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  134. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  135. package/tests-wtr/heuristics/heuristics-utils.test.ts +49 -0
  136. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  137. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  138. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  139. package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
@@ -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');
@@ -4,6 +4,7 @@ import {
4
4
  cleanButtonText,
5
5
  classifyButtonTextRegex,
6
6
  classifyButtons,
7
+ classifyPopup,
7
8
  isExcludedPopup,
8
9
  } from '../../lib/heuristic-classify';
9
10
  import { getActionablePopups, isDisabled, excludeContainers, getButtonData, isDialogLikeElement } from '../../lib/heuristics';
@@ -387,6 +388,38 @@ describe('classifyButtonTextRegex', () => {
387
388
  expect(classifyButtonTextRegex('Сохранить и закрыть')).to.equal('acknowledge');
388
389
  });
389
390
 
391
+ it('matches Catalan acknowledge buttons', () => {
392
+ expect(classifyButtonTextRegex('Entenc')).to.equal('acknowledge');
393
+ expect(classifyButtonTextRegex('Ho entenc')).to.equal('acknowledge');
394
+ expect(classifyButtonTextRegex('Entès!')).to.equal('acknowledge');
395
+ expect(classifyButtonTextRegex('Entesos')).to.equal('acknowledge');
396
+ });
397
+
398
+ it('matches Catalan necessary-only buttons as reject', () => {
399
+ expect(classifyButtonTextRegex('Només les necessàries')).to.equal('reject');
400
+ expect(classifyButtonTextRegex('Només necessàries')).to.equal('reject');
401
+ expect(classifyButtonTextRegex('Necessàries')).to.equal('reject');
402
+ expect(classifyButtonTextRegex('Acceptar només les galetes necessàries')).to.equal('reject');
403
+ expect(classifyButtonTextRegex('Només galetes tècniques')).to.equal('reject');
404
+ expect(classifyButtonTextRegex('Només les essencials')).to.equal('reject');
405
+ expect(classifyButtonTextRegex('Continuar sense acceptar')).to.equal('reject');
406
+ });
407
+
408
+ it('matches Catalan settings buttons', () => {
409
+ expect(classifyButtonTextRegex('Configuració')).to.equal('settings');
410
+ expect(classifyButtonTextRegex('Personalitzar')).to.equal('settings');
411
+ expect(classifyButtonTextRegex('Preferències')).to.equal('settings');
412
+ expect(classifyButtonTextRegex('Més opcions')).to.equal('settings');
413
+ expect(classifyButtonTextRegex('Configuració de galetes')).to.equal('settings');
414
+ expect(classifyButtonTextRegex('Gestionar les meves preferències')).to.equal('settings');
415
+ expect(classifyButtonTextRegex('Gestiona les galetes')).to.equal('settings');
416
+ });
417
+
418
+ it('does not treat Catalan save or accept buttons as settings or reject', () => {
419
+ expect(classifyButtonTextRegex('Desar preferències')).to.not.equal('settings');
420
+ expect(classifyButtonTextRegex('Acceptar')).to.equal('accept');
421
+ });
422
+
390
423
  it('does not treat Russian subscription buttons as reject', () => {
391
424
  expect(classifyButtonTextRegex('Отказаться от подписки')).to.equal('other');
392
425
  });
@@ -412,6 +445,22 @@ describe('classifyButtonTextRegex', () => {
412
445
  });
413
446
  });
414
447
 
448
+ describe('classifyPopup', () => {
449
+ function classify(texts: string[]) {
450
+ return classifyPopup(texts.map((text) => ({ text, regexClassification: classifyButtonTextRegex(text) })));
451
+ }
452
+
453
+ it('dismisses a Catalan notice-only banner as tier1', () => {
454
+ expect(classify(['Entenc'])).to.equal('tier1');
455
+ });
456
+
457
+ it('does not acknowledge a Catalan banner that offers necessary-only or settings', () => {
458
+ expect(classify(['Entès', 'Només les necessàries'])).to.equal('reject');
459
+ expect(classify(['Entès', 'Preferències'])).to.equal('none');
460
+ expect(classify(['Entès', 'Personalitzar'])).to.equal('none');
461
+ });
462
+ });
463
+
415
464
  describe('classifyButtons', () => {
416
465
  it('separates reject buttons from other buttons', () => {
417
466
  const buttons: ButtonData[] = [
@@ -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,2 +0,0 @@
1
- import generateCMPTests from '../../playwright/runner';
2
- generateCMPTests('auto_CA_plex.tv_ee4', ['https://www.plex.tv/'], { testOptIn: false, testSelfTest: true, onlyRegions: ['CA'] });
@@ -1,6 +0,0 @@
1
- import generateCMPTests from '../../playwright/runner';
2
- generateCMPTests('auto_CA_support.plex.tv_gn3', ['https://support.plex.tv/'], {
3
- testOptIn: false,
4
- testSelfTest: true,
5
- onlyRegions: ['CA'],
6
- });