diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a72f6cd6..1bf8daa2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - v2 permissions: contents: read diff --git a/.releaserc b/.releaserc index 904333e4..8d5c9353 100644 --- a/.releaserc +++ b/.releaserc @@ -1,13 +1,16 @@ { - "branches": ["main"], + "branches": ["main", "v2"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/npm", + ["@semantic-release/exec", { + "prepareCmd": "sed -i \"s/export const VERSION = '.*'/export const VERSION = '${nextRelease.version}'/\" src/index.js" + }], ["@semantic-release/git", { - "assets": ["CHANGELOG.md", "package.json"], + "assets": ["CHANGELOG.md", "package.json", "src/index.js"], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" }] ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 52dc5738..48767bdb 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ The AEM Experimentation plugin helps you quickly set up experimentation and segm It is currently available to customers in collaboration with AEM Engineering via co-innovation VIP Projects. To implement experimentation or personalization use-cases, please reach out to the AEM Engineering team in the Slack channel dedicated to your project. +> **Note:** We are adding new support for the contextual experimentation rail UI. This is still under development. Feel free to reach out if you have any questions via email: aem-contextual-experimentation@adobe.com. + ## Features The AEM Experimentation plugin supports: @@ -16,11 +18,13 @@ The AEM Experimentation plugin supports: ## Installation Add the plugin to your AEM project by running: + ```sh git subtree add --squash --prefix plugins/experimentation git@github.com:adobe/aem-experimentation.git v2 ``` -If you later want to pull the latest changes and update your local copy of the plugin +If you later want to pull the latest changes and update your local copy of the plugin: + ```sh git subtree pull --squash --prefix plugins/experimentation git@github.com:adobe/aem-experimentation.git v2 ``` @@ -29,85 +33,90 @@ If you prefer using `https` links you'd replace `git@github.com:adobe/aem-experi ## Project instrumentation -### On top of a regular boilerplate project +### Key Files to Add or Modify -Typically, you'd know you don't have the plugin system if you don't see a reference to `window.aem.plugins` or `window.hlx.plugins` in your `scripts.js`. In that case, you can still manually instrument this plugin in your project by falling back to a more manual instrumentation. To properly connect and configure the plugin for your project, you'll need to edit your `scripts.js` in your AEM project and add the following: +1. **plugins/experimentation** - Add this folder containing the experimentation engine plugins (see Installation section above) +2. **scripts/experiment-loader.js** - Add this script to handle experiment loading +3. **scripts/scripts.js** - Modify this script with the configuration -1. at the start of the file: - ```js - const experimentationConfig = { - prodHost: 'www.my-site.com', - audiences: { - mobile: () => window.innerWidth < 600, - desktop: () => window.innerWidth >= 600, - // define your custom audiences here as needed - } - }; +### Step 1: Create `scripts/experiment-loader.js` - let runExperimentation; - let showExperimentationOverlay; - const isExperimentationEnabled = document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"],[property^="campaign:"],[property^="audience:"]') - || [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i)); - if (isExperimentationEnabled) { - ({ - loadEager: runExperimentation, - loadLazy: showExperimentationOverlay, - } = await import('../plugins/experimentation/src/index.js')); - } - ``` -2. Early in the `loadEager` method you'll need to add: - ```js - async function loadEager(doc) { - … - // Add below snippet early in the eager phase - if (runExperimentation) { - await runExperimentation(document, experimentationConfig); - } - … - } - ``` - This needs to be done as early as possible since this will be blocking the eager phase and impacting your LCP, so we want this to execute as soon as possible. -3. Finally at the end of the `loadLazy` method you'll have to add: - ```js - async function loadLazy(doc) { - … - // Add below snippet at the end of the lazy phase - if (showExperimentationOverlay) { - await showExperimentationOverlay(document, experimentationConfig); +Create a new file `scripts/experiment-loader.js` with the following content: + +```js +/** + * Checks if experimentation is enabled. + * @returns {boolean} True if experimentation is enabled, false otherwise. + */ +const isExperimentationEnabled = () => document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"],[property^="campaign:"],[property^="audience:"]') + || [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i)); + +/** + * Loads the experimentation module (eager). + * @param {Document} document The document object. + * @param {Object} config The experimentation configuration. + * @returns {Promise} A promise that resolves when the experimentation module is loaded. + */ +export async function runExperimentation(document, config) { + if (!isExperimentationEnabled()) { + window.addEventListener('message', async (event) => { + if (event.data?.type === 'hlx:experimentation-get-config') { + event.source.postMessage({ + type: 'hlx:experimentation-config', + config: { experiments: [], audiences: [], campaigns: [] }, + source: 'no-experiments' + }, '*'); } - } - ``` - This is mostly used for the authoring overlay, and as such isn't essential to the page rendering, so having it at the end of the lazy phase is good enough. + }); + return null; + } + + try { + const { loadEager } = await import( + '../plugins/experimentation/src/index.js' + ); + return loadEager(document, config); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to load experimentation module (eager):', error); + return null; + } +} -### On top of the plugin system (deprecated) +``` + +> **Note:** Add the following line to your `head.html` to preload the experiment loader script: +> ```html +> +> ``` -The easiest way to add the plugin is if your project is set up with the plugin system extension in the boilerplate. -You'll know you have it if either `window.aem.plugins` or `window.hlx.plugins` is defined on your page. +### Step 2: Update `scripts/scripts.js` -If you don't have it, you can follow the proposal in https://github.com/adobe/aem-lib/pull/23 and https://github.com/adobe/aem-boilerplate/pull/275 and apply the changes to your `aem.js`/`lib-franklin.js` and `scripts.js`. +Add the following import and configuration at the top of your `scripts/scripts.js`: -Once you have confirmed this, you'll need to edit your `scripts.js` in your AEM project and add the following at the start of the file: ```js +import { + runExperimentation, +} from './experiment-loader.js'; + const experimentationConfig = { - prodHost: 'www.my-site.com', + prodHost: 'www.mysite.com', // add your prodHost here, otherwise we will show mock data audiences: { mobile: () => window.innerWidth < 600, desktop: () => window.innerWidth >= 600, // define your custom audiences here as needed - } + }, }; +``` -window.aem.plugins.add('experimentation', { // use window.hlx instead of your project has this - condition: () => - // page level metadata - document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"]') - // decorated section metadata - || document.querySelector('.section[class*=experiment],.section[class*=audience],.section[class*=campaign]') - // undecorated section metadata - || [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i)), - options: experimentationConfig, - url: '/plugins/experimentation/src/index.js', -}); +Then, add the following line early in your `loadEager()` function: + +```js +async function loadEager(doc) { + // ... existing code ... + await runExperimentation(doc, experimentationConfig); + // ... rest of your code ... +} ``` ### Increasing sampling rate for low traffic pages @@ -138,14 +147,14 @@ If this is not present, please apply the following changes to the file: https:// ### Custom options -There are various aspects of the plugin that you can configure via options you are passing to the 2 main methods above (`runEager`/`runLazy`). +There are various aspects of the plugin that you can configure via the `experimentationConfig` object. You have already seen the `audiences` option in the examples above, but here is the full list we support: ```js -runEager.call(document, { +const experimentationConfig = { // Lets you configure the prod environment. // (prod environments do not get the pill overlay) - prodHost: 'www.my-website.com', + prodHost: 'www.mysite.com', // if you have several, or need more complex logic to toggle pill overlay, you can use isProd: () => !window.location.hostname.endsWith('hlx.page') && window.location.hostname !== ('localhost'), @@ -177,7 +186,7 @@ runEager.call(document, { buildBlock(el); decorateBlock(el); } -}); +}; ``` For detailed implementation instructions on the different features, please read the dedicated pages we have on those topics: @@ -678,7 +687,7 @@ Here's the complete experiment config structure available in `window.hlx.experim variantNames: ["control", "challenger-1"], audiences: ["mobile", "desktop"], resolvedAudiences: ["mobile"], - requiresConsent: false, // whether this experiment requires user consent + requiresConsent: true, // whether this experiment requires user consent run: true, variants: { control: { percentageSplit: "0.5", pages: ["/current"], label: "Control" }, diff --git a/package-lock.json b/package-lock.json index f03bb53a..6e7abecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@babel/eslint-parser": "7.22.15", "@playwright/test": "1.44.0", "@semantic-release/changelog": "6.0.3", + "@semantic-release/exec": "6.0.3", "@semantic-release/git": "10.0.1", "@semantic-release/npm": "12.0.1", "eslint": "8.48.0", @@ -1090,6 +1091,174 @@ "node": ">=18" } }, + "node_modules/@semantic-release/exec": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-6.0.3.tgz", + "integrity": "sha512-bxAq8vLOw76aV89vxxICecEa8jfaWwYITw6X74zzlO0mc/Bgieqx9kBRz9z96pHectiTAtsCwsQcUyLYWnp3VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.4", + "parse-json": "^5.0.0" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/exec/node_modules/@semantic-release/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@semantic-release/exec/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/exec/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@semantic-release/exec/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@semantic-release/exec/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@semantic-release/exec/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/exec/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@semantic-release/exec/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/exec/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/exec/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@semantic-release/exec/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@semantic-release/git": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", diff --git a/package.json b/package.json index d8bf5ce6..0358fc38 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@babel/eslint-parser": "7.22.15", "@playwright/test": "1.44.0", "@semantic-release/changelog": "6.0.3", + "@semantic-release/exec": "6.0.3", "@semantic-release/git": "10.0.1", "@semantic-release/npm": "12.0.1", "eslint": "8.48.0", diff --git a/src/index.js b/src/index.js index d23ee216..459e645d 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,8 @@ export function debug(...args) { } } +export const VERSION = '1.0.1'; + export const DEFAULT_OPTIONS = { // Audiences related properties @@ -696,13 +698,33 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { return null; } + const thumbnailMeta = document.querySelector('meta[property="og:image:secure_url"]') + || document.querySelector('meta[property="og:image"]'); + const thumbnail = thumbnailMeta ? thumbnailMeta.getAttribute('content') : ''; + const audiences = stringToArray(metadata.audiences).map(toClassName); const splits = metadata.split - // custom split - ? stringToArray(metadata.split).map((i) => parseFloat(i) / 100) - // even split - : [...new Array(pages.length)].map(() => 1 / (pages.length + 1)); + ? (() => { + const splitValues = stringToArray(metadata.split).map( + (i) => parseFloat(i) / 100, + ); + + // If fewer splits than pages, pad with zeros + if (splitValues.length < pages.length) { + return [ + ...splitValues, + ...Array(pages.length - splitValues.length).fill(0), + ]; + } + + // If more splits than needed, truncate + if (splitValues.length > pages.length) { + return splitValues.slice(0, pages.length); + } + + return splitValues; + })() : [...new Array(pages.length)].map(() => 1 / (pages.length + 1)); const variantNames = []; variantNames.push('control'); @@ -745,15 +767,17 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) { const config = { id, - label: `Experiment ${metadata.value || metadata.experiment}`, + label: metadata.label || `Experiment ${metadata.value || metadata.experiment}`, status: metadata.status || 'active', audiences, requiresConsent, endDate, + optimizingTarget: metadata.optimizingTarget || 'conversion', resolvedAudiences, startDate, variants, variantNames, + thumbnail, }; config.run = ( @@ -1016,11 +1040,69 @@ async function serveAudience(document, pluginOptions) { ); } +// Support new Rail UI communication +function setupCommunicationLayer(options) { + window.addEventListener('message', async (event) => { + if (event.data && event.data.type === 'hlx:last-modified-request') { + const { url } = event.data; + + try { + const response = await fetch(url, { + method: 'HEAD', + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache', + }, + }); + + const lastModified = response.headers.get('Last-Modified'); + + event.source.postMessage( + { + type: 'hlx:last-modified-response', + url, + lastModified, + status: response.status, + }, + event.origin, + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching Last-Modified header:', error); + } + } else if (event.data?.type === 'hlx:experimentation-get-config') { + try { + const safeClone = JSON.parse(JSON.stringify(window.hlx)); + if (options.prodHost) { + safeClone.prodHost = options.prodHost; + } + event.source.postMessage( + { + type: 'hlx:experimentation-config', + config: safeClone, + source: 'index-js', + }, + '*', + ); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error sending hlx config:', e); + } + } else if ( + event.data?.type === 'hlx:experimentation-window-reload' + && event.data?.action === 'reload' + ) { + window.location.reload(); + } + }); +} + export async function loadEager(document, options = {}) { const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; setDebugMode(window.location, pluginOptions); const ns = window.aem || window.hlx || {}; + ns.experimentation = { version: VERSION }; ns.audiences = await serveAudience(document, pluginOptions); ns.experiments = await runExperiment(document, pluginOptions); ns.campaigns = await runCampaign(document, pluginOptions); @@ -1029,20 +1111,12 @@ export async function loadEager(document, options = {}) { ns.experiment = ns.experiments.find((e) => e.type === 'page'); ns.audience = ns.audiences.find((e) => e.type === 'page'); ns.campaign = ns.campaigns.find((e) => e.type === 'page'); -} -export async function loadLazy(document, options = {}) { - const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; - // do not show the experimentation pill on prod domains - if (!isDebugEnabled) { - return; + if (isDebugEnabled) { + setupCommunicationLayer(pluginOptions); } - // eslint-disable-next-line import/no-unresolved - const preview = await import('https://opensource.adobe.com/aem-experimentation/preview.js'); - const context = { - getMetadata, - toClassName, - debug, - }; - preview.default.call(context, document, pluginOptions); +} + +export async function loadLazy() { + // Placeholder for lazy loading functionality } diff --git a/tests/experiments.test.js b/tests/experiments.test.js index e6380ada..6d5e7eed 100644 --- a/tests/experiments.test.js +++ b/tests/experiments.test.js @@ -354,3 +354,99 @@ test.describe('Backward Compatibility with v1', () => { expect(await page.locator('main').textContent()).toEqual('Hello v1!'); }); }); + +test.describe('Consent Management', () => { + test('isUserConsentGiven returns false by default', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const hasConsent = await page.evaluate(() => localStorage.getItem('experimentation-consented') === 'true'); + expect(hasConsent).toBe(false); + }); + + test('updateUserConsent stores consent in localStorage', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + await page.evaluate(() => localStorage.setItem('experimentation-consented', 'true')); + const stored = await page.evaluate(() => localStorage.getItem('experimentation-consented')); + expect(stored).toBe('true'); + }); + + test('updateUserConsent removes consent from localStorage when false', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + await page.evaluate(() => { + localStorage.setItem('experimentation-consented', 'true'); + localStorage.removeItem('experimentation-consented'); + }); + const stored = await page.evaluate(() => localStorage.getItem('experimentation-consented')); + expect(stored).toBeNull(); + }); + + test('isUserConsentGiven returns true after consent is granted', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const hasConsent = await page.evaluate(() => { + localStorage.setItem('experimentation-consented', 'true'); + return localStorage.getItem('experimentation-consented') === 'true'; + }); + expect(hasConsent).toBe(true); + }); + + test('consent persists across page reloads', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + await page.evaluate(() => localStorage.setItem('experimentation-consented', 'true')); + await page.reload(); + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const hasConsent = await page.evaluate(() => localStorage.getItem('experimentation-consented') === 'true'); + expect(hasConsent).toBe(true); + }); + + test('experiment does not run when consent is required but not given', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level--requires-consent'); + expect(await page.locator('main').textContent()).toEqual('Hello World!'); + }); + + test('experiment runs when consent is required and given', async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem('experimentation-consented', 'true'); + }); + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level--requires-consent?experiment=foo/challenger-1'); + expect(await page.locator('main').textContent()).toEqual('Hello v1!'); + }); + + test('experiment config includes requiresConsent flag', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level--requires-consent'); + const config = await page.evaluate(() => window.hlx.experiments?.[0]?.config); + expect(config?.requiresConsent).toBe(true); + }); + + test('experiments without requiresConsent metadata run normally', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level?experiment=foo/challenger-1'); + expect(await page.locator('main').textContent()).toEqual('Hello v1!'); + const config = await page.evaluate(() => window.hlx.experiments?.[0]?.config); + expect(config?.requiresConsent).toBe(false); + }); +}); + +test.describe('Experiment Configuration', () => { + test('experiment config includes thumbnail from og:image', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const config = await page.evaluate(() => window.hlx.experiments?.[0]?.config); + expect(config).toHaveProperty('thumbnail'); + }); + + test('experiment config includes optimizingTarget', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const config = await page.evaluate(() => window.hlx.experiments?.[0]?.config); + expect(config.optimizingTarget).toBe('conversion'); + }); + + test('experiment config includes label', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const config = await page.evaluate(() => window.hlx.experiments?.[0]?.config); + expect(config.label).toMatch(/Experiment foo/); + }); + + test('variant labels use custom names when provided', async ({ page }) => { + await goToAndRunExperiment(page, '/tests/fixtures/experiments/page-level'); + const config = await page.evaluate(() => window.hlx.experiments?.[0]?.config); + expect(config.variants['challenger-1'].label).toBe('V1'); + expect(config.variants['challenger-2'].label).toBe('V2'); + }); +}); diff --git a/tests/fixtures/experiments/page-level--requires-consent.html b/tests/fixtures/experiments/page-level--requires-consent.html new file mode 100644 index 00000000..27940913 --- /dev/null +++ b/tests/fixtures/experiments/page-level--requires-consent.html @@ -0,0 +1,13 @@ + + + + + + + + + +
Hello World!
+ + +