@duckduckgo/autoconsent 16.59.0 → 16.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/lib/regional-testing/harness.mjs +4 -172
- package/.agents/skills/oxylabs-testing/SKILL.md +4 -4
- package/.agents/skills/oxylabs-testing/scripts/oxylabs.mjs +17 -164
- package/.agents/skills/oxylabs-testing/scripts/oxylabs.test.mjs +6 -3
- package/.agents/skills/verify-pr/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +107 -0
- package/Jenkinsfile +36 -1
- package/dist/addon-firefox/background.bundle.js +21 -0
- package/dist/addon-firefox/compact-rules.json +1 -1
- package/dist/addon-firefox/content.bundle.js +253 -93
- package/dist/addon-firefox/manifest.json +1 -1
- package/dist/addon-firefox/rule-index.json +1 -1
- package/dist/addon-firefox/rules.json +1 -1
- package/dist/addon-mv3/background.bundle.js +21 -0
- package/dist/addon-mv3/compact-rules.json +1 -1
- package/dist/addon-mv3/content.bundle.js +253 -93
- package/dist/addon-mv3/manifest.json +1 -1
- package/dist/addon-mv3/rule-index.json +1 -1
- package/dist/addon-mv3/rules.json +1 -1
- package/dist/autoconsent.cjs.js +253 -93
- package/dist/autoconsent.esm.js +253 -93
- package/dist/autoconsent.playwright.js +253 -93
- package/dist/autoconsent.standalone.js +254 -94
- package/dist/heuristics.cjs.js +29 -3
- package/dist/heuristics.mjs +28 -3
- package/dist/types/cmps/onetrust.d.ts +1 -0
- package/dist/types/eval-snippets.d.ts +3 -0
- package/dist/types/heuristic-classify.d.ts +1 -0
- package/dist/types/heuristics.d.ts +10 -1
- package/lib/cmps/onetrust.ts +15 -0
- package/lib/eval-snippets.ts +21 -0
- package/lib/heuristic-classify.ts +6 -0
- package/lib/heuristic-patterns.ts +26 -1
- package/lib/heuristics.ts +256 -104
- package/package.json +1 -1
- package/playwright/isolated-world.mjs +217 -0
- package/playwright/oxylabs.mjs +196 -0
- package/playwright/runner.ts +183 -39
- package/playwright.config.ts +4 -1
- package/rules/autoconsent/aeolservice.json +39 -0
- package/rules/autoconsent/airbnb.json +32 -0
- package/rules/autoconsent/amazon-music.json +13 -0
- package/rules/autoconsent/annunci69.json +19 -0
- package/rules/autoconsent/appconsent.json +6 -1
- package/rules/autoconsent/arbeidsplassen-nav-no.json +33 -0
- package/rules/autoconsent/att.json +36 -0
- package/rules/autoconsent/auctionet.json +39 -0
- package/rules/autoconsent/autodoc.json +18 -0
- package/rules/autoconsent/bbc.com-orb-cosmetic.json +34 -0
- package/rules/autoconsent/bbc.com.json +8 -5
- package/rules/autoconsent/bokadirekt.se.json +50 -0
- package/rules/autoconsent/bouvet-cookiepanel.json +29 -0
- package/rules/autoconsent/com-consent-layer.json +10 -0
- package/rules/autoconsent/consent-studio.json +25 -13
- package/rules/autoconsent/cookielegit.json +21 -0
- package/rules/autoconsent/cookieprompter.json +21 -0
- package/rules/autoconsent/cookiereports.json +10 -0
- package/rules/autoconsent/cookietractor.json +37 -0
- package/rules/autoconsent/de-volksbank.json +14 -0
- package/rules/autoconsent/dpgmedia-nl.json +20 -8
- package/rules/autoconsent/eon.pl.json +18 -0
- package/rules/autoconsent/eroticiracconti-it.json +13 -0
- package/rules/autoconsent/ersatzteile-24.json +49 -0
- package/rules/autoconsent/fastweb-cookie-center.json +13 -0
- package/rules/autoconsent/futurabolzano-it.json +35 -0
- package/rules/autoconsent/gobasic.json +10 -0
- package/rules/autoconsent/gravito.json +23 -16
- package/rules/autoconsent/hacon-next-gen.json +41 -0
- package/rules/autoconsent/ing-de.json +31 -0
- package/rules/autoconsent/interia.json +1 -4
- package/rules/autoconsent/intesasanpaolo.json +37 -0
- package/rules/autoconsent/jobindex.json +40 -0
- package/rules/autoconsent/kaspersky-cookies-notification.json +9 -0
- package/rules/autoconsent/lantmateriet.json +13 -0
- package/rules/autoconsent/livsmedelsverket.json +18 -0
- package/rules/autoconsent/mainichi.json +39 -0
- package/rules/autoconsent/mango.json +56 -0
- package/rules/autoconsent/nederlandseloterij.json +35 -0
- package/rules/autoconsent/newsbreak.json +16 -0
- package/rules/autoconsent/nhsapp.json +10 -0
- package/rules/autoconsent/nordea.json +21 -0
- package/rules/autoconsent/nordnet.json +33 -0
- package/rules/autoconsent/ok.json +4 -12
- package/rules/autoconsent/onlineradiobox.json +35 -0
- package/rules/autoconsent/pe-cookies.json +30 -0
- package/rules/autoconsent/pensionsmyndigheten.json +13 -0
- package/rules/autoconsent/pro-euclp-cookies-box.json +31 -0
- package/rules/autoconsent/pubfactory.json +45 -0
- package/rules/autoconsent/puesc-gov-pl.json +37 -0
- package/rules/autoconsent/rambler.json +30 -0
- package/rules/autoconsent/rejestr-io.json +36 -0
- package/rules/autoconsent/sas.json +6 -6
- package/rules/autoconsent/scb.se.json +13 -0
- package/rules/autoconsent/schumacher.com.json +53 -0
- package/rules/autoconsent/sitevision.json +18 -1
- package/rules/autoconsent/skatteverket-se.json +33 -0
- package/rules/autoconsent/smile-insurances.json +13 -0
- package/rules/autoconsent/studentapan.json +16 -0
- package/rules/autoconsent/svt.se.json +7 -7
- package/rules/autoconsent/temu.json +12 -16
- package/rules/autoconsent/thecode.json +21 -0
- package/rules/autoconsent/tv2reg-cookie-notice.json +39 -0
- package/rules/autoconsent/uk-parliament.json +19 -0
- package/rules/autoconsent/wirtualna-polska.json +13 -10
- package/rules/autoconsent/xenforo-cookie-consent.json +62 -0
- package/rules/autoconsent/zorginstituut-cookiewarning.json +33 -0
- package/rules/compact-rules.json +1 -1
- package/rules/rule-index.json +1 -1
- package/rules/rules.json +1 -1
- package/tests/aeolservice.spec.ts +5 -0
- package/tests/airbnb.spec.ts +3 -0
- package/tests/amazon-music.spec.ts +9 -0
- package/tests/annunci69.spec.ts +5 -0
- package/tests/appconsent.spec.ts +6 -0
- package/tests/arbeidsplassen-nav-no.spec.ts +3 -0
- package/tests/att.spec.ts +3 -0
- package/tests/auctionet.spec.ts +3 -0
- package/tests/autodoc.spec.ts +8 -0
- package/tests/bbc.com-orb-cosmetic.spec.ts +8 -0
- package/tests/bbc.com.spec.ts +11 -9
- package/tests/bokadirekt.se.spec.ts +3 -0
- package/tests/bouvet-cookiepanel.spec.ts +9 -0
- package/tests/com-consent-layer.spec.ts +6 -0
- package/tests/consent-studio.spec.ts +5 -0
- package/tests/cookiebot.spec.ts +1 -0
- package/tests/cookielegit.spec.ts +3 -0
- package/tests/cookieprompter.spec.ts +9 -0
- package/tests/cookiereports.spec.ts +13 -0
- package/tests/cookietractor.spec.ts +12 -0
- package/tests/de-volksbank.spec.ts +5 -0
- package/tests/dpgmedia-nl.spec.ts +15 -1
- package/tests/eon.pl.spec.ts +3 -0
- package/tests/eroticiracconti-it.spec.ts +3 -0
- package/tests/ersatzteile-24.spec.ts +6 -0
- package/tests/fastweb-cookie-center.spec.ts +3 -0
- package/tests/futurabolzano-it.spec.ts +3 -0
- package/tests/gobasic.spec.ts +3 -0
- package/tests/gravito.spec.ts +7 -1
- package/tests/hacon-next-gen.spec.ts +6 -0
- package/tests/ing-de.spec.ts +3 -0
- package/tests/interia.spec.ts +1 -1
- package/tests/intesasanpaolo.spec.ts +3 -0
- package/tests/jobindex.spec.ts +3 -0
- package/tests/kaspersky-cookies-notification.spec.ts +8 -0
- package/tests/lantmateriet.spec.ts +3 -0
- package/tests/livsmedelsverket.spec.ts +5 -0
- package/tests/mainichi.spec.ts +10 -0
- package/tests/mango.spec.ts +3 -0
- package/tests/nederlandseloterij.spec.ts +9 -0
- package/tests/newsbreak.spec.ts +3 -0
- package/tests/nhsapp.spec.ts +3 -0
- package/tests/nordea.spec.ts +6 -0
- package/tests/nordnet.spec.ts +8 -0
- package/tests/onlineradiobox.spec.ts +5 -0
- package/tests/pe-cookies.spec.ts +6 -0
- package/tests/pensionsmyndigheten.spec.ts +3 -0
- package/tests/pro-euclp-cookies-box.spec.ts +8 -0
- package/tests/pubfactory.spec.ts +10 -0
- package/tests/puesc-gov-pl.spec.ts +3 -0
- package/tests/rambler.spec.ts +3 -0
- package/tests/rejestr-io.spec.ts +3 -0
- package/tests/sas.spec.ts +1 -1
- package/tests/scb.se.spec.ts +4 -0
- package/tests/schumacher.com.spec.ts +6 -0
- package/tests/sitevision-cookie-consent.spec.ts +1 -0
- package/tests/sitevision.spec.ts +7 -0
- package/tests/skatteverket-se.spec.ts +3 -0
- package/tests/smile-insurances.spec.ts +6 -0
- package/tests/studentapan.spec.ts +3 -0
- package/tests/svt.se.spec.ts +1 -3
- package/tests/temu.spec.ts +3 -1
- package/tests/thecode.spec.ts +3 -0
- package/tests/tv2reg-cookie-notice.spec.ts +16 -0
- package/tests/uk-parliament.spec.ts +8 -0
- package/tests/wirtualna-polska.spec.ts +6 -1
- package/tests/xenforo-cookie-consent.spec.ts +6 -0
- package/tests/zorginstituut-cookiewarning.spec.ts +10 -0
- package/tests-wtr/heuristics/get-potential-popups.test.ts +330 -9
- package/tests-wtr/heuristics/heuristics-utils.test.ts +98 -0
- package/rules/autoconsent/twitter.json +0 -34
- package/rules/generated/auto_AU_flysas.com_r1s.json +0 -40
- package/rules/generated/auto_CH_airbnb.de_83z.json +0 -40
- package/rules/generated/auto_CH_autodoc.de_krd.json +0 -40
- package/rules/generated/auto_CH_svtplay.se_d39.json +0 -40
- package/rules/generated/auto_GB_parliamentlive.tv_r3v.json +0 -28
- package/rules/generated/auto_NL_asnbank.nl_e28.json +0 -40
- package/rules/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.json +0 -40
- package/rules/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.json +0 -40
- package/rules/generated/auto_NL_nederlandseloterij.nl_b60.json +0 -40
- package/rules/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.json +0 -40
- package/rules/generated/auto_NO_arronet.se_vo5.json +0 -40
- package/rules/generated/auto_NO_liseberg.se_qy4.json +0 -40
- package/rules/generated/auto_NO_nordnet.se_2en.json +0 -40
- package/rules/generated/auto_NO_regeringen.se_uhv.json +0 -40
- package/rules/generated/auto_NO_svtplay.se_ujn.json +0 -40
- package/rules/generated/auto_NO_uu.se_eea.json +0 -40
- package/rules/generated/auto_US_newsbreak.com_0.json +0 -28
- package/tests/generated/auto_AU_flysas.com_r1s.spec.ts +0 -2
- package/tests/generated/auto_CH_airbnb.de_83z.spec.ts +0 -2
- package/tests/generated/auto_CH_autodoc.de_krd.spec.ts +0 -2
- package/tests/generated/auto_CH_svtplay.se_d39.spec.ts +0 -2
- package/tests/generated/auto_GB_parliamentlive.tv_r3v.spec.ts +0 -6
- package/tests/generated/auto_NL_asnbank.nl_e28.spec.ts +0 -6
- package/tests/generated/auto_NL_eurojackpot.nederlandseloterij.nl_7hr.spec.ts +0 -6
- package/tests/generated/auto_NL_lotto.nederlandseloterij.nl_7c8.spec.ts +0 -6
- package/tests/generated/auto_NL_nederlandseloterij.nl_b60.spec.ts +0 -6
- package/tests/generated/auto_NL_staatsloterij.nederlandseloterij.nl_197.spec.ts +0 -6
- package/tests/generated/auto_NO_arronet.se_vo5.spec.ts +0 -2
- package/tests/generated/auto_NO_liseberg.se_qy4.spec.ts +0 -2
- package/tests/generated/auto_NO_nordnet.se_2en.spec.ts +0 -2
- package/tests/generated/auto_NO_regeringen.se_uhv.spec.ts +0 -6
- package/tests/generated/auto_NO_svtplay.se_ujn.spec.ts +0 -2
- package/tests/generated/auto_NO_uu.se_eea.spec.ts +0 -2
- package/tests/generated/auto_US_newsbreak.com_0.spec.ts +0 -2
- package/tests/twitter.spec.ts +0 -10
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oxylabs Agent Browser connection: region targeting, the CDP endpoint, and credential redaction.
|
|
3
|
+
* Docs: https://developers.oxylabs.io/products/agent-browser
|
|
4
|
+
* Shared by the Playwright E2E runner and the `oxylabs-testing` skill.
|
|
5
|
+
*
|
|
6
|
+
* Requires env vars: OXYLABS_USER, OXYLABS_PASSWORD.
|
|
7
|
+
* Optional: OXYLABS_HOST (defaults to hb.oxylabs.io).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('@playwright/test').Browser} Browser
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {'desktop'|'mobile'} OxylabsDevice
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} OxylabsBrowserOptions
|
|
20
|
+
* @property {OxylabsDevice} [device] - Oxylabs ?p_device= value. Oxylabs defaults to 'desktop'.
|
|
21
|
+
* @property {boolean} [solveCaptcha=true] - Send ?solve_captcha=true: let Oxylabs solve captchas on the page.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { chromium } from '@playwright/test';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* US state- and city-targeted region keys, in addition to any two-letter country code. Query params
|
|
28
|
+
* follow https://developers.oxylabs.io/products/agent-browser/geolocation-and-proxy-selection
|
|
29
|
+
* (`p_state` is US-only; `p_city` needs `p_cc` or `p_state`).
|
|
30
|
+
* @type {Record<string, Record<string, string>>}
|
|
31
|
+
*/
|
|
32
|
+
export const OXYLABS_LOCAL_REGIONS = {
|
|
33
|
+
'us-california': { p_state: 'california' },
|
|
34
|
+
'us-newyork': { p_cc: 'US', p_city: 'new_york' },
|
|
35
|
+
'us-losangeles': { p_cc: 'US', p_city: 'los_angeles' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} regionKey - A two-letter country code (e.g. 'de', 'pt') or a key of OXYLABS_LOCAL_REGIONS.
|
|
40
|
+
* @returns {Record<string, string>}
|
|
41
|
+
*/
|
|
42
|
+
export function oxylabsRegionParams(regionKey) {
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(OXYLABS_LOCAL_REGIONS, regionKey)) {
|
|
44
|
+
return OXYLABS_LOCAL_REGIONS[regionKey];
|
|
45
|
+
}
|
|
46
|
+
if (/^[a-z]{2}$/i.test(regionKey)) {
|
|
47
|
+
return { p_cc: regionKey.toUpperCase() };
|
|
48
|
+
}
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unknown Oxylabs region "${regionKey}". Use a two-letter country code or one of: ${Object.keys(OXYLABS_LOCAL_REGIONS).join(', ')}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
56
|
+
*/
|
|
57
|
+
export function isOxylabsConfigured(env = process.env) {
|
|
58
|
+
return Boolean(env.OXYLABS_USER && env.OXYLABS_PASSWORD);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the Oxylabs CDP endpoint for a region. The result contains credentials: never log it.
|
|
63
|
+
* @param {string} regionKey
|
|
64
|
+
* @param {OxylabsBrowserOptions} [opts]
|
|
65
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
66
|
+
*/
|
|
67
|
+
export function buildOxylabsEndpoint(regionKey, opts = {}, env = process.env) {
|
|
68
|
+
const user = env.OXYLABS_USER;
|
|
69
|
+
const password = env.OXYLABS_PASSWORD;
|
|
70
|
+
const host = env.OXYLABS_HOST || 'hb.oxylabs.io';
|
|
71
|
+
if (!user || !password) {
|
|
72
|
+
throw new Error('Missing Oxylabs credentials. Set OXYLABS_USER and OXYLABS_PASSWORD.');
|
|
73
|
+
}
|
|
74
|
+
const params = new URLSearchParams(oxylabsRegionParams(regionKey));
|
|
75
|
+
if (opts.device) {
|
|
76
|
+
params.set('p_device', opts.device);
|
|
77
|
+
}
|
|
78
|
+
// Accounts without captcha solving get HTTP 400 for this param, so only send it when asked.
|
|
79
|
+
if (opts.solveCaptcha ?? true) {
|
|
80
|
+
params.set('solve_captcha', 'true');
|
|
81
|
+
}
|
|
82
|
+
// user/password are interpolated raw — Oxylabs expects literal characters in
|
|
83
|
+
// userinfo (e.g. '+' must NOT be percent-encoded), matching their Python/JS
|
|
84
|
+
// SDK examples. If your credentials contain ':' or '@', escape upstream.
|
|
85
|
+
return `wss://${user}:${password}@${host}/?${params.toString()}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Playwright puts the endpoint URL in connection error messages; strip the credentials.
|
|
90
|
+
* @param {string} message
|
|
91
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
92
|
+
*/
|
|
93
|
+
export function redactCredentials(message, env = process.env) {
|
|
94
|
+
let out = message;
|
|
95
|
+
for (const secret of [env.OXYLABS_PASSWORD, env.OXYLABS_USER]) {
|
|
96
|
+
if (secret) out = out.split(secret).join('***');
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Connect to an Oxylabs remote browser for a specific region.
|
|
103
|
+
* @param {string} regionKey
|
|
104
|
+
* @param {OxylabsBrowserOptions} [opts]
|
|
105
|
+
* @returns {Promise<Browser>}
|
|
106
|
+
*/
|
|
107
|
+
export async function connectOxylabs(regionKey, opts = {}) {
|
|
108
|
+
try {
|
|
109
|
+
return await chromium.connectOverCDP(buildOxylabsEndpoint(regionKey, opts), { timeout: 30000 });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// The first line has the cause; the call log after it repeats the endpoint URL.
|
|
112
|
+
const message = redactCredentials((e instanceof Error ? e.message : String(e)).split('\n')[0]);
|
|
113
|
+
const hint =
|
|
114
|
+
(opts.solveCaptcha ?? true) && message.includes('400') ? ' (is captcha solving enabled for this Oxylabs account?)' : '';
|
|
115
|
+
// eslint-disable-next-line preserve-caught-error -- the original error carries the credentials
|
|
116
|
+
throw new Error(message + hint);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Captcha events arrive as window messages from the Oxylabs runtime. A listener in autoconsent's
|
|
121
|
+
// isolated world forwards them over its CDP binding: Agent Browser drops main-world bindings, so
|
|
122
|
+
// page.exposeBinding can't deliver them. See https://developers.oxylabs.io/products/agent-browser/captcha-handling
|
|
123
|
+
const CAPTCHA_MESSAGE = 'oxylabsCaptcha';
|
|
124
|
+
// Agent Browser sends `oxylabs-captcha-solve-start`; the docs list `-start`, `-end`, `-solve-end` and `-error`.
|
|
125
|
+
const CAPTCHA_START_EVENTS = ['oxylabs-captcha-start', 'oxylabs-captcha-solve-start'];
|
|
126
|
+
const CAPTCHA_END_EVENTS = ['oxylabs-captcha-end', 'oxylabs-captcha-solve-end'];
|
|
127
|
+
const CAPTCHA_ERROR_EVENTS = ['oxylabs-captcha-error', 'oxylabs-captcha-solve-error'];
|
|
128
|
+
// Longest a test pauses for solving, counted from the first start event: the wait in Oxylabs' example.
|
|
129
|
+
const CAPTCHA_SOLVE_TIMEOUT_MS = 60000;
|
|
130
|
+
const captchaListenerScript = `
|
|
131
|
+
window.addEventListener("message", (e) => {
|
|
132
|
+
if (e?.data?.source === "oxylabs-runtime" && typeof e.data.type === "string") {
|
|
133
|
+
window.autoconsentSendMessage({ type: "${CAPTCHA_MESSAGE}", event: e.data.type });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Tracks the Oxylabs captcha solver for one page. Run `script` in each isolated world next to the
|
|
140
|
+
* content script and pass every message from it to `handleMessage`.
|
|
141
|
+
* @typedef {Object} CaptchaTracker
|
|
142
|
+
* @property {{ detected: boolean, solved: boolean|null }} state - The solver state.
|
|
143
|
+
* @property {Promise<void>} done - Resolves when solving ends (success or error).
|
|
144
|
+
* @property {() => boolean} isSolving - Whether waits should pause for the solver.
|
|
145
|
+
* @property {string} script
|
|
146
|
+
* @property {(msg: any) => boolean} handleMessage - Returns true for a captcha event.
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @returns {CaptchaTracker}
|
|
151
|
+
*/
|
|
152
|
+
export function createCaptchaTracker() {
|
|
153
|
+
/** @type {{ detected: boolean, solved: boolean|null }} */
|
|
154
|
+
const state = { detected: false, solved: null };
|
|
155
|
+
/** @type {() => void} */
|
|
156
|
+
let resolveDone = () => {};
|
|
157
|
+
/** @type {Promise<void>} */
|
|
158
|
+
const done = new Promise((resolve) => {
|
|
159
|
+
resolveDone = resolve;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let solving = false;
|
|
163
|
+
/** @type {number|null} */
|
|
164
|
+
let firstStartAt = null;
|
|
165
|
+
|
|
166
|
+
function handleCaptchaEvent(/** @type {string} */ type) {
|
|
167
|
+
if (CAPTCHA_START_EVENTS.includes(type)) {
|
|
168
|
+
state.detected = true;
|
|
169
|
+
solving = true;
|
|
170
|
+
firstStartAt ??= Date.now();
|
|
171
|
+
} else if (CAPTCHA_END_EVENTS.includes(type)) {
|
|
172
|
+
state.detected = true;
|
|
173
|
+
state.solved = true;
|
|
174
|
+
solving = false;
|
|
175
|
+
resolveDone();
|
|
176
|
+
} else if (CAPTCHA_ERROR_EVENTS.includes(type)) {
|
|
177
|
+
state.detected = true;
|
|
178
|
+
state.solved = false;
|
|
179
|
+
solving = false;
|
|
180
|
+
resolveDone();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
state,
|
|
186
|
+
done,
|
|
187
|
+
// Oxylabs gives no deadline for a start event, so waits pause whenever one arrives.
|
|
188
|
+
isSolving: () => solving && firstStartAt !== null && Date.now() - firstStartAt < CAPTCHA_SOLVE_TIMEOUT_MS,
|
|
189
|
+
script: captchaListenerScript,
|
|
190
|
+
handleMessage: (msg) => {
|
|
191
|
+
if (msg?.type !== CAPTCHA_MESSAGE) return false;
|
|
192
|
+
handleCaptchaEvent(msg.event);
|
|
193
|
+
return true;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
package/playwright/runner.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { test, expect, Page, Frame, TestInfo } from '@playwright/test';
|
|
5
|
-
import { waitFor } from '../lib/utils';
|
|
4
|
+
import { test as base, expect, Page, Frame, TestInfo, Response } from '@playwright/test';
|
|
6
5
|
import { ContentScriptMessage } from '../lib/messages';
|
|
7
6
|
import { AutoAction, RuleBundle } from '../lib/types';
|
|
8
7
|
import { filterCompactRules } from '../lib/encoding';
|
|
@@ -10,6 +9,8 @@ import compactRules from '../rules/compact-rules.json';
|
|
|
10
9
|
// Full rules include optIn steps that compact rules intentionally omit.
|
|
11
10
|
// We need both formats: compact for optOut tests, full for optIn tests.
|
|
12
11
|
import { autoconsent as fullRules } from '../rules/rules.json';
|
|
12
|
+
import { injectIntoIsolatedWorld } from './isolated-world.mjs';
|
|
13
|
+
import { connectOxylabs, createCaptchaTracker } from './oxylabs.mjs';
|
|
13
14
|
|
|
14
15
|
const LOG_MESSAGES: ContentScriptMessage['type'][] = process.env.CI
|
|
15
16
|
? []
|
|
@@ -17,8 +18,53 @@ const LOG_MESSAGES: ContentScriptMessage['type'][] = process.env.CI
|
|
|
17
18
|
const LOG_PAGE_LOGS = false;
|
|
18
19
|
|
|
19
20
|
const testRegion = (process.env.REGION || 'NA').trim();
|
|
21
|
+
|
|
22
|
+
// OXYLABS=1 runs each test in a fresh Oxylabs Agent Browser session in REGION instead of a local browser.
|
|
23
|
+
const useOxylabs = process.env.OXYLABS === '1';
|
|
24
|
+
// Local runs list the tests that failed on a bot wall here; Oxylabs runs only define the listed tests.
|
|
25
|
+
const oxylabsCandidatesFile = process.env.OXYLABS_CANDIDATES;
|
|
26
|
+
const oxylabsCandidates = useOxylabs && oxylabsCandidatesFile ? readOxylabsCandidates(oxylabsCandidatesFile) : null;
|
|
27
|
+
const recordOxylabsCandidates = !useOxylabs && !!oxylabsCandidatesFile;
|
|
28
|
+
// Pages load slower through remote Oxylabs sessions, so detection gets a longer window there.
|
|
29
|
+
const detectionSlowdown = useOxylabs ? 2 : 1;
|
|
30
|
+
// Status codes that bot protection answers with; other errors (404, 5xx) point to a stale spec URL instead.
|
|
31
|
+
const BOT_WALL_STATUSES = [401, 403, 429, 503];
|
|
32
|
+
// Navigation errors of a dead or misconfigured site rather than a blocked one.
|
|
33
|
+
const DEAD_SITE_ERRORS = /ERR_NAME_NOT_RESOLVED|ERR_SOCKS_CONNECTION_FAILED|ERR_CONNECTION_REFUSED|ERR_ADDRESS_UNREACHABLE|ERR_CERT_/;
|
|
34
|
+
// Page titles of common bot-protection challenge and block pages.
|
|
35
|
+
const BOT_WALL_TITLE =
|
|
36
|
+
/just a moment|attention required|client challenge|access denied|access to this page has been denied|pardon our interruption|request unsuccessful|are you a robot|verify you are human|security check|captcha/i;
|
|
37
|
+
|
|
38
|
+
const test = useOxylabs
|
|
39
|
+
? base.extend({
|
|
40
|
+
page: async ({ browserName }, use) => {
|
|
41
|
+
if (browserName !== 'chromium') {
|
|
42
|
+
throw new Error('Oxylabs runs need a Chromium project');
|
|
43
|
+
}
|
|
44
|
+
const browser = await connectOxylabs(testRegion.toLowerCase());
|
|
45
|
+
try {
|
|
46
|
+
await use(await browser.newPage());
|
|
47
|
+
} finally {
|
|
48
|
+
await browser.close().catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
: base;
|
|
20
53
|
test.describe.configure({ mode: 'parallel' });
|
|
21
54
|
|
|
55
|
+
// The CMP name keeps tests for the same site in different specs apart.
|
|
56
|
+
function oxylabsCandidateKey(cmp: string, testName: string) {
|
|
57
|
+
return `${cmp} › ${testName}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readOxylabsCandidates(file: string) {
|
|
61
|
+
try {
|
|
62
|
+
return new Set(fs.readFileSync(file, 'utf8').split('\n').filter(Boolean));
|
|
63
|
+
} catch {
|
|
64
|
+
return new Set<string>();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
type TestOptions = {
|
|
23
69
|
testOptOut: boolean;
|
|
24
70
|
testSelfTest: boolean;
|
|
@@ -30,6 +76,13 @@ type TestOptions = {
|
|
|
30
76
|
expectedRuns: number;
|
|
31
77
|
gpc: boolean;
|
|
32
78
|
};
|
|
79
|
+
// A frame's content script, reached through either the isolated-world transport or a main-world Frame.
|
|
80
|
+
type ContentScriptTarget = {
|
|
81
|
+
send: (message: object) => Promise<unknown>;
|
|
82
|
+
evalInMainWorld: (code: string) => Promise<unknown>;
|
|
83
|
+
isMainFrame: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
33
86
|
const defaultOptions: TestOptions = {
|
|
34
87
|
testOptOut: true,
|
|
35
88
|
testOptIn: true,
|
|
@@ -43,7 +96,7 @@ const defaultOptions: TestOptions = {
|
|
|
43
96
|
};
|
|
44
97
|
|
|
45
98
|
const contentScript = fs.readFileSync(path.join(__dirname, '../dist/autoconsent.playwright.js'), 'utf8');
|
|
46
|
-
const screenshotsDir = path.join(__dirname, '../test-results/screenshots');
|
|
99
|
+
const screenshotsDir = path.join(__dirname, '../test-results/screenshots', useOxylabs ? 'oxylabs' : '');
|
|
47
100
|
const deduplicatedRuleLookup = (
|
|
48
101
|
JSON.parse(fs.readFileSync(path.join(__dirname, '../rules/rules.json'), 'utf-8')) as RuleBundle
|
|
49
102
|
).autoconsent.reduce((acc, rule) => {
|
|
@@ -57,6 +110,7 @@ class TestRun {
|
|
|
57
110
|
page: Page;
|
|
58
111
|
testInfo: TestInfo;
|
|
59
112
|
url: string;
|
|
113
|
+
cmp: string;
|
|
60
114
|
expectedCmp: string;
|
|
61
115
|
options: TestOptions;
|
|
62
116
|
autoAction: AutoAction | null;
|
|
@@ -65,12 +119,16 @@ class TestRun {
|
|
|
65
119
|
formFactor: string;
|
|
66
120
|
received: ContentScriptMessage[] = [];
|
|
67
121
|
screenshotCounter = 0;
|
|
68
|
-
|
|
122
|
+
selfTestTarget: ContentScriptTarget | null = null;
|
|
123
|
+
response: Response | null = null;
|
|
124
|
+
// Oxylabs solves captchas on the page; waits pause while it does.
|
|
125
|
+
captcha = useOxylabs ? createCaptchaTracker() : null;
|
|
69
126
|
|
|
70
127
|
constructor(page: Page, testInfo: TestInfo, url: string, expectedCmp: string, options: TestOptions, autoAction: AutoAction | null) {
|
|
71
128
|
this.page = page;
|
|
72
129
|
this.testInfo = testInfo;
|
|
73
130
|
this.url = url;
|
|
131
|
+
this.cmp = expectedCmp;
|
|
74
132
|
// if a rule was deduplicated, update the expectation to the new deduplicate rule name
|
|
75
133
|
if (deduplicatedRuleLookup.has(expectedCmp)) {
|
|
76
134
|
this.expectedCmp = deduplicatedRuleLookup.get(expectedCmp)!;
|
|
@@ -94,15 +152,43 @@ class TestRun {
|
|
|
94
152
|
console.log(` page log:`, msg.text());
|
|
95
153
|
});
|
|
96
154
|
|
|
97
|
-
|
|
155
|
+
// Chromium runs the content script in an isolated world like the extension; other browsers have no CDP for that.
|
|
156
|
+
const useIsolatedWorld = this.page.context().browser()?.browserType().name() === 'chromium';
|
|
157
|
+
if (useIsolatedWorld) {
|
|
158
|
+
await injectIntoIsolatedWorld(
|
|
159
|
+
this.page,
|
|
160
|
+
(transport) => async (msg, frameRef) => {
|
|
161
|
+
if (this.captcha?.handleMessage(msg)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await this.messageCallback(msg, {
|
|
165
|
+
send: (message) => transport.sendToContentScript(frameRef, message),
|
|
166
|
+
evalInMainWorld: (code) => transport.evalInMainWorld(frameRef, code),
|
|
167
|
+
isMainFrame: transport.isMainFrame(frameRef),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
contentScript,
|
|
171
|
+
this.captcha?.script,
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
await this.page.exposeBinding('autoconsentSendMessage', ({ frame }, msg: ContentScriptMessage) =>
|
|
175
|
+
this.messageCallback(msg, {
|
|
176
|
+
send: (message) => frame.evaluate(`autoconsentReceiveMessage(${JSON.stringify(message)})`),
|
|
177
|
+
evalInMainWorld: (code) => frame.evaluate(code),
|
|
178
|
+
isMainFrame: frame.parentFrame() === null,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
98
182
|
if (this.options.gpc) {
|
|
99
183
|
await this.enableGpc();
|
|
100
184
|
}
|
|
101
|
-
|
|
185
|
+
try {
|
|
186
|
+
this.response = await this.page.goto(this.url, { waitUntil: 'commit' });
|
|
102
187
|
|
|
103
|
-
|
|
188
|
+
if (!useIsolatedWorld) {
|
|
189
|
+
await this.injectContentScripts();
|
|
190
|
+
}
|
|
104
191
|
|
|
105
|
-
try {
|
|
106
192
|
await this.runAssertions();
|
|
107
193
|
} catch (e) {
|
|
108
194
|
if (e instanceof Error) {
|
|
@@ -115,6 +201,7 @@ class TestRun {
|
|
|
115
201
|
formFactor: this.formFactor,
|
|
116
202
|
testName: this.testInfo.title,
|
|
117
203
|
retry: this.testInfo.retry,
|
|
204
|
+
provider: useOxylabs ? 'oxylabs' : 'local',
|
|
118
205
|
};
|
|
119
206
|
// log the full url in the error message, this will be parsed by the review tool
|
|
120
207
|
console.error(`Autoconsent test failed on ${this.url} failure stats: ${JSON.stringify(failureStats)}`);
|
|
@@ -128,6 +215,34 @@ class TestRun {
|
|
|
128
215
|
}
|
|
129
216
|
}
|
|
130
217
|
|
|
218
|
+
// On the last attempt, list a failed test that hit a bot wall so the Oxylabs pass can rerun it.
|
|
219
|
+
// Called from afterEach, which also runs when the test times out before reaching its own catch.
|
|
220
|
+
async recordOxylabsCandidate() {
|
|
221
|
+
const { status, retry, project, error } = this.testInfo;
|
|
222
|
+
if (status === 'passed' || status === 'skipped' || status === 'interrupted' || retry < project.retries) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (await this.looksBlocked(error?.message ?? '')) {
|
|
226
|
+
fs.appendFileSync(oxylabsCandidatesFile!, `${oxylabsCandidateKey(this.cmp, this.testInfo.title)}\n`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The site served a bot wall instead of its content: the page never loaded (other than a dead host),
|
|
231
|
+
// or nothing was detected on a bot-wall status code or a challenge page.
|
|
232
|
+
async looksBlocked(errorMessage: string) {
|
|
233
|
+
if (!this.response) {
|
|
234
|
+
return !DEAD_SITE_ERRORS.test(errorMessage);
|
|
235
|
+
}
|
|
236
|
+
if (this.isMessageReceived({ type: 'cmpDetected' })) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (BOT_WALL_STATUSES.includes(this.response.status())) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
const title = await this.page.title().catch(() => '');
|
|
243
|
+
return BOT_WALL_TITLE.test(title);
|
|
244
|
+
}
|
|
245
|
+
|
|
131
246
|
// emulate a browser that sends the Global Privacy Control signal
|
|
132
247
|
async enableGpc() {
|
|
133
248
|
await this.page.setExtraHTTPHeaders({ 'Sec-GPC': '1' });
|
|
@@ -159,29 +274,29 @@ class TestRun {
|
|
|
159
274
|
}
|
|
160
275
|
}
|
|
161
276
|
|
|
162
|
-
async messageCallback(
|
|
277
|
+
async messageCallback(msg: ContentScriptMessage, target: ContentScriptTarget) {
|
|
163
278
|
LOG_MESSAGES.includes(msg.type) && console.log(msg);
|
|
164
279
|
this.received.push(msg);
|
|
165
280
|
switch (msg.type) {
|
|
166
281
|
case 'init': {
|
|
167
|
-
const url = frame.url();
|
|
168
|
-
const mainFrame = frame.parentFrame() === null;
|
|
169
282
|
// Use full rules for optIn (compact rules omit optIn steps), compact rules for optOut.
|
|
170
283
|
const rules =
|
|
171
284
|
this.autoAction === 'optIn'
|
|
172
285
|
? { autoconsent: fullRules }
|
|
173
|
-
: { compact: filterCompactRules(compactRules, { url, mainFrame }) };
|
|
174
|
-
await
|
|
175
|
-
|
|
286
|
+
: { compact: filterCompactRules(compactRules, { url: msg.url, mainFrame: target.isMainFrame }) };
|
|
287
|
+
await target.send({
|
|
288
|
+
type: 'initResp',
|
|
289
|
+
config: {
|
|
176
290
|
enabled: true,
|
|
177
291
|
autoAction: this.autoAction,
|
|
178
292
|
disabledCmps: [],
|
|
179
293
|
enablePrehide: false,
|
|
180
|
-
detectRetries: 20,
|
|
294
|
+
detectRetries: 20 * detectionSlowdown,
|
|
181
295
|
enableCosmeticRules: true,
|
|
182
296
|
visualTest: true,
|
|
183
|
-
}
|
|
184
|
-
|
|
297
|
+
},
|
|
298
|
+
rules,
|
|
299
|
+
});
|
|
185
300
|
break;
|
|
186
301
|
}
|
|
187
302
|
case 'cmpDetected': {
|
|
@@ -196,20 +311,25 @@ class TestRun {
|
|
|
196
311
|
case 'optOutResult': {
|
|
197
312
|
await this.takeScreenshot(`${this.screenshotCounter++}-result`);
|
|
198
313
|
if (msg.scheduleSelfTest) {
|
|
199
|
-
this.
|
|
314
|
+
this.selfTestTarget = target;
|
|
200
315
|
}
|
|
201
316
|
break;
|
|
202
317
|
}
|
|
203
318
|
case 'autoconsentDone': {
|
|
204
319
|
await this.takeScreenshot(`${this.screenshotCounter++}-done`);
|
|
205
|
-
if (this.
|
|
206
|
-
await this.
|
|
320
|
+
if (this.selfTestTarget && this.options.testSelfTest) {
|
|
321
|
+
await this.selfTestTarget.send({ type: 'selfTest' });
|
|
207
322
|
}
|
|
208
323
|
break;
|
|
209
324
|
}
|
|
210
325
|
case 'eval': {
|
|
211
|
-
|
|
212
|
-
|
|
326
|
+
let result: unknown = false;
|
|
327
|
+
try {
|
|
328
|
+
result = await target.evalInMainWorld(msg.code);
|
|
329
|
+
} catch {
|
|
330
|
+
// the frame may have navigated or detached
|
|
331
|
+
}
|
|
332
|
+
await target.send({ id: msg.id, type: 'evalResp', result });
|
|
213
333
|
break;
|
|
214
334
|
}
|
|
215
335
|
case 'visualDelay': {
|
|
@@ -240,8 +360,19 @@ class TestRun {
|
|
|
240
360
|
return this.findReceivedMessages(msg).length > 0;
|
|
241
361
|
}
|
|
242
362
|
|
|
243
|
-
waitForMessage(msg: Partial<ContentScriptMessage>, maxTimes = 50, interval = 500) {
|
|
244
|
-
|
|
363
|
+
async waitForMessage(msg: Partial<ContentScriptMessage>, maxTimes = 50, interval = 500) {
|
|
364
|
+
let attempts = 0;
|
|
365
|
+
while (!this.isMessageReceived(msg)) {
|
|
366
|
+
if (attempts >= maxTimes) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
370
|
+
// time spent waiting for the captcha solver doesn't count
|
|
371
|
+
if (!this.captcha?.isSolving()) {
|
|
372
|
+
attempts++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
245
376
|
}
|
|
246
377
|
|
|
247
378
|
async assertMessageReceived(
|
|
@@ -282,7 +413,7 @@ class TestRun {
|
|
|
282
413
|
}
|
|
283
414
|
|
|
284
415
|
async runAssertions() {
|
|
285
|
-
await this.assertMessageReceived(`no CMP detected`, { type: 'cmpDetected' });
|
|
416
|
+
await this.assertMessageReceived(`no CMP detected`, { type: 'cmpDetected' }, true, 50 * detectionSlowdown);
|
|
286
417
|
|
|
287
418
|
const expectedCmpDetected: Partial<ContentScriptMessage> = { type: 'cmpDetected', cmp: this.expectedCmp };
|
|
288
419
|
await this.assertMessageReceived(`detected a wrong CMP`, expectedCmpDetected);
|
|
@@ -320,7 +451,7 @@ class TestRun {
|
|
|
320
451
|
await this.assertMessageReceived(`optInResult not received`, { type: 'optInResult' }, true, 1, 300);
|
|
321
452
|
await this.assertMessageReceived(`optInResult received, but failed`, { type: 'optInResult', result: true }, true, 1, 300);
|
|
322
453
|
}
|
|
323
|
-
if (this.options.testSelfTest && this.
|
|
454
|
+
if (this.options.testSelfTest && this.selfTestTarget) {
|
|
324
455
|
await this.assertMessageReceived(`selfTestResult not received`, { type: 'selfTestResult' }, true, 1, 300);
|
|
325
456
|
await this.assertMessageReceived(
|
|
326
457
|
`selfTestResult received, but failed`,
|
|
@@ -340,8 +471,19 @@ class TestRun {
|
|
|
340
471
|
}
|
|
341
472
|
}
|
|
342
473
|
|
|
474
|
+
// Runs by test id, for the afterEach hook that records Oxylabs candidates.
|
|
475
|
+
const testRuns = new Map<string, TestRun>();
|
|
476
|
+
|
|
343
477
|
export default function generateCMPTests(cmp: string, sites: string[], overrideOptions: Partial<TestOptions> = {}) {
|
|
344
478
|
test.describe(cmp, () => {
|
|
479
|
+
if (recordOxylabsCandidates) {
|
|
480
|
+
// eslint-disable-next-line no-empty-pattern
|
|
481
|
+
test.afterEach(async ({}, testInfo) => {
|
|
482
|
+
const testRun = testRuns.get(testInfo.testId);
|
|
483
|
+
testRuns.delete(testInfo.testId);
|
|
484
|
+
await testRun?.recordOxylabsCandidate();
|
|
485
|
+
});
|
|
486
|
+
}
|
|
345
487
|
sites.forEach((url) => {
|
|
346
488
|
const finalOptions = { ...defaultOptions, ...overrideOptions };
|
|
347
489
|
if (finalOptions.onlyRegions && finalOptions.onlyRegions.length > 0 && !finalOptions.onlyRegions.includes(testRegion)) {
|
|
@@ -355,26 +497,28 @@ export default function generateCMPTests(cmp: string, sites: string[], overrideO
|
|
|
355
497
|
const urlHash = crypto.createHash('md5').update(url).digest('hex').slice(0, 4);
|
|
356
498
|
const formFactor = finalOptions.mobile ? 'mobile' : 'desktop';
|
|
357
499
|
|
|
358
|
-
|
|
359
|
-
const testName = `${domain} ${urlHash} .${testRegion}
|
|
500
|
+
const defineTest = (label: string, autoAction: AutoAction | null) => {
|
|
501
|
+
const testName = `${domain} ${urlHash} .${testRegion} ${label} ${formFactor}`;
|
|
502
|
+
if (oxylabsCandidates && !oxylabsCandidates.has(oxylabsCandidateKey(cmp, testName))) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
360
505
|
test(testName, async ({ page }, testInfo) => {
|
|
361
|
-
const testRun = new TestRun(page, testInfo, url, cmp, finalOptions,
|
|
506
|
+
const testRun = new TestRun(page, testInfo, url, cmp, finalOptions, autoAction);
|
|
507
|
+
if (recordOxylabsCandidates) {
|
|
508
|
+
testRuns.set(testInfo.testId, testRun);
|
|
509
|
+
}
|
|
362
510
|
await testRun.run();
|
|
363
511
|
});
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
if (!finalOptions.testOptIn && !finalOptions.testOptOut) {
|
|
515
|
+
defineTest('noaction', null);
|
|
364
516
|
}
|
|
365
517
|
if (finalOptions.testOptIn) {
|
|
366
|
-
|
|
367
|
-
test(testName, async ({ page }, testInfo) => {
|
|
368
|
-
const testRun = new TestRun(page, testInfo, url, cmp, finalOptions, 'optIn');
|
|
369
|
-
await testRun.run();
|
|
370
|
-
});
|
|
518
|
+
defineTest('optIn', 'optIn');
|
|
371
519
|
}
|
|
372
520
|
if (finalOptions.testOptOut) {
|
|
373
|
-
|
|
374
|
-
test(testName, async ({ page }, testInfo) => {
|
|
375
|
-
const testRun = new TestRun(page, testInfo, url, cmp, finalOptions, 'optOut');
|
|
376
|
-
await testRun.run();
|
|
377
|
-
});
|
|
521
|
+
defineTest('optOut', 'optOut');
|
|
378
522
|
}
|
|
379
523
|
});
|
|
380
524
|
});
|
package/playwright.config.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { PlaywrightTestConfig, devices } from '@playwright/test';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Oxylabs sessions bring their own regional proxy and reject contexts that set another one.
|
|
4
|
+
const proxy = process.env.PROXY_SERVER && process.env.OXYLABS !== '1' ? { server: process.env.PROXY_SERVER } : undefined;
|
|
4
5
|
|
|
5
6
|
const config: PlaywrightTestConfig = {
|
|
6
7
|
forbidOnly: !!process.env.CI,
|
|
7
8
|
retries: process.env.CI ? 2 : 0,
|
|
9
|
+
// Remote Oxylabs sessions load pages more slowly and may wait up to 60s for the captcha solver.
|
|
10
|
+
timeout: process.env.OXYLABS === '1' ? 240000 : undefined,
|
|
8
11
|
testDir: 'tests',
|
|
9
12
|
use: {
|
|
10
13
|
trace: 'off',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aeolservice",
|
|
3
|
+
"vendorUrl": "https://cloud.aeolservice.es/",
|
|
4
|
+
"runContext": {
|
|
5
|
+
"urlPattern": "^https?://cloud\\.aeolservice\\.es/"
|
|
6
|
+
},
|
|
7
|
+
"prehideSelectors": [],
|
|
8
|
+
"detectCmp": [
|
|
9
|
+
{
|
|
10
|
+
"exists": "#cookies_consent_div"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"detectPopup": [
|
|
14
|
+
{
|
|
15
|
+
"visible": "#popup_content.cookies_consent_div .config_cookies"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"optIn": [
|
|
19
|
+
{
|
|
20
|
+
"waitForThenClick": "#popup_content.cookies_consent_div .explanation .aceptar_cokies"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"optOut": [
|
|
24
|
+
{
|
|
25
|
+
"waitForThenClick": "#popup_content.cookies_consent_div .config_cookies"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"waitForVisible": "#popup_content #configurar_cookies"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"click": "#popup_content .cookie_button:not(.cookies_tecnicas) input[type=checkbox]:checked",
|
|
32
|
+
"all": true,
|
|
33
|
+
"optional": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"waitForThenClick": "#popup_content .not_explanation.aceptar_cokies"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "airbnb",
|
|
3
|
+
"vendorUrl": "https://www.airbnb.com/",
|
|
4
|
+
"runContext": { "urlPattern": "^https?://(www\\.)?airbnb\\.[a-z.]+/" },
|
|
5
|
+
"prehideSelectors": ["[data-testid=\"main-cookies-banner-container\"]"],
|
|
6
|
+
"detectCmp": [
|
|
7
|
+
{ "exists": "[data-testid=\"main-cookies-banner-container\"] section > div > div:nth-child(2) > div:nth-child(2) > button" }
|
|
8
|
+
],
|
|
9
|
+
"detectPopup": [
|
|
10
|
+
{ "visible": "[data-testid=\"main-cookies-banner-container\"] section > div > div:nth-child(2) > div:nth-child(2) > button" }
|
|
11
|
+
],
|
|
12
|
+
"optIn": [
|
|
13
|
+
{
|
|
14
|
+
"waitForThenClick": "[data-testid=\"main-cookies-banner-container\"] section > div > div:nth-child(2) > div:nth-child(1) > button"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"optOut": [
|
|
18
|
+
{
|
|
19
|
+
"waitForThenClick": "[data-testid=\"main-cookies-banner-container\"] section > div > div:nth-child(2) > div:nth-child(2) > button",
|
|
20
|
+
"retry": 5,
|
|
21
|
+
"retryInterval": 500,
|
|
22
|
+
"comment": "Only necessary; retry in case handlers are not attached yet"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"test": [
|
|
26
|
+
{
|
|
27
|
+
"waitForVisible": "[data-testid=\"main-cookies-banner-container\"] section > div > div:nth-child(2) > div:nth-child(2) > button",
|
|
28
|
+
"timeout": 1000,
|
|
29
|
+
"check": "none"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|